fix(nextjs): produce correct next.config.js file for production server (#15824)
This commit is contained in:
parent
5e5a3f7487
commit
c7e49c564a
@ -5,6 +5,7 @@ import {
|
|||||||
cleanupProject,
|
cleanupProject,
|
||||||
getPackageManagerCommand,
|
getPackageManagerCommand,
|
||||||
isNotWindows,
|
isNotWindows,
|
||||||
|
killPort,
|
||||||
killPorts,
|
killPorts,
|
||||||
newProject,
|
newProject,
|
||||||
packageManagerLockFile,
|
packageManagerLockFile,
|
||||||
@ -20,25 +21,22 @@ import {
|
|||||||
} from '@nrwl/e2e/utils';
|
} from '@nrwl/e2e/utils';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import { checkApp } from './utils';
|
import { checkApp } from './utils';
|
||||||
|
import { removeSync } from 'fs-extra';
|
||||||
|
|
||||||
describe('Next.js Applications', () => {
|
describe('Next.js Applications', () => {
|
||||||
let proj: string;
|
let proj: string;
|
||||||
let originalEnv: string;
|
let originalEnv: string;
|
||||||
let packageManager;
|
let packageManager;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeEach(() => {
|
||||||
proj = newProject();
|
proj = newProject();
|
||||||
packageManager = detectPackageManager(tmpProjPath());
|
packageManager = detectPackageManager(tmpProjPath());
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => cleanupProject());
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
originalEnv = process.env.NODE_ENV;
|
originalEnv = process.env.NODE_ENV;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
process.env.NODE_ENV = originalEnv;
|
process.env.NODE_ENV = originalEnv;
|
||||||
|
cleanupProject();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate app + libs', async () => {
|
it('should generate app + libs', async () => {
|
||||||
@ -169,6 +167,22 @@ describe('Next.js Applications', () => {
|
|||||||
`dist/apps/${appName}/public/a/b.txt`,
|
`dist/apps/${appName}/public/a/b.txt`,
|
||||||
`dist/apps/${appName}/public/shared/ui/hello.txt`
|
`dist/apps/${appName}/public/shared/ui/hello.txt`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check that the output is self-contained (i.e. can run with its own package.json + node_modules)
|
||||||
|
const distPath = joinPathFragments(tmpProjPath(), 'dist/apps', appName);
|
||||||
|
const port = 3000;
|
||||||
|
const pmc = getPackageManagerCommand();
|
||||||
|
runCommand(`${pmc.install}`, {
|
||||||
|
cwd: distPath,
|
||||||
|
});
|
||||||
|
runCLI(
|
||||||
|
`generate @nrwl/workspace:run-commands serve-prod --project ${appName} --cwd=dist/apps/${appName} --command="npx next start --port=${port}"`
|
||||||
|
);
|
||||||
|
await runCommandUntil(`run ${appName}:serve-prod`, (output) => {
|
||||||
|
return output.includes(`localhost:${port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await killPort(port);
|
||||||
}, 300_000);
|
}, 300_000);
|
||||||
|
|
||||||
it('should build and install pruned lock file', () => {
|
it('should build and install pruned lock file', () => {
|
||||||
@ -399,9 +413,10 @@ describe('Next.js Applications', () => {
|
|||||||
}, 300_000);
|
}, 300_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
function getData(port: number, path = ''): Promise<any> {
|
function getData(port, path = ''): Promise<any> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`http://localhost:${port}${path}`, (res) => {
|
http
|
||||||
|
.get(`http://localhost:${port}${path}`, (res) => {
|
||||||
expect(res.statusCode).toEqual(200);
|
expect(res.statusCode).toEqual(200);
|
||||||
let data = '';
|
let data = '';
|
||||||
res.on('data', (chunk) => {
|
res.on('data', (chunk) => {
|
||||||
@ -410,6 +425,7 @@ function getData(port: number, path = ''): Promise<any> {
|
|||||||
res.once('end', () => {
|
res.once('end', () => {
|
||||||
resolve(data);
|
resolve(data);
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
.on('error', (err) => reject(err));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,22 +35,21 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-proposal-decorators": "^7.14.5",
|
"@babel/plugin-proposal-decorators": "^7.14.5",
|
||||||
"@nrwl/cypress": "file:../cypress",
|
|
||||||
"@nrwl/devkit": "file:../devkit",
|
"@nrwl/devkit": "file:../devkit",
|
||||||
"@nrwl/jest": "file:../jest",
|
|
||||||
"@nrwl/js": "file:../js",
|
"@nrwl/js": "file:../js",
|
||||||
"@nrwl/linter": "file:../linter",
|
"@nrwl/linter": "file:../linter",
|
||||||
"@nrwl/react": "file:../react",
|
"@nrwl/react": "file:../react",
|
||||||
"@nrwl/webpack": "file:../webpack",
|
|
||||||
"@nrwl/workspace": "file:../workspace",
|
"@nrwl/workspace": "file:../workspace",
|
||||||
"@svgr/webpack": "^6.1.2",
|
"@svgr/webpack": "^6.1.2",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
|
"copy-webpack-plugin": "^10.2.4",
|
||||||
"dotenv": "~10.0.0",
|
"dotenv": "~10.0.0",
|
||||||
"fs-extra": "^11.1.0",
|
"fs-extra": "^11.1.0",
|
||||||
"ignore": "^5.0.4",
|
"ignore": "^5.0.4",
|
||||||
"semver": "7.3.4",
|
"semver": "7.3.4",
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"tsconfig-paths": "^4.1.2",
|
"tsconfig-paths": "^4.1.2",
|
||||||
|
"tsconfig-paths-webpack-plugin": "4.0.0",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"webpack-merge": "^5.8.0"
|
"webpack-merge": "^5.8.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,8 +13,9 @@ import {
|
|||||||
DependentBuildableProjectNode,
|
DependentBuildableProjectNode,
|
||||||
} from '@nrwl/js/src/utils/buildable-libs-utils';
|
} from '@nrwl/js/src/utils/buildable-libs-utils';
|
||||||
import type { NextConfig } from 'next';
|
import type { NextConfig } from 'next';
|
||||||
|
import { PHASE_PRODUCTION_SERVER } from 'next/constants';
|
||||||
|
|
||||||
import path = require('path');
|
import * as path from 'path';
|
||||||
import { createWebpackConfig } from '../src/utils/config';
|
import { createWebpackConfig } from '../src/utils/config';
|
||||||
import { NextBuildBuilderOptions } from '../src/utils/types';
|
import { NextBuildBuilderOptions } from '../src/utils/types';
|
||||||
export interface WithNxOptions extends NextConfig {
|
export interface WithNxOptions extends NextConfig {
|
||||||
@ -113,8 +114,14 @@ function getNxContext(
|
|||||||
export function withNx(
|
export function withNx(
|
||||||
_nextConfig = {} as WithNxOptions,
|
_nextConfig = {} as WithNxOptions,
|
||||||
context: WithNxContext = getWithNxContext()
|
context: WithNxContext = getWithNxContext()
|
||||||
): () => Promise<NextConfig> {
|
): (phase: string) => Promise<NextConfig> {
|
||||||
return async () => {
|
return async (phase: string) => {
|
||||||
|
if (phase === PHASE_PRODUCTION_SERVER) {
|
||||||
|
// If we are running an already built production server, just return the configuration.
|
||||||
|
const { nx, ...validNextConfig } = _nextConfig;
|
||||||
|
return { distDir: '.next', ...validNextConfig };
|
||||||
|
} else {
|
||||||
|
// Otherwise, add in webpack and eslint configuration for build or test.
|
||||||
let dependencies: DependentBuildableProjectNode[] = [];
|
let dependencies: DependentBuildableProjectNode[] = [];
|
||||||
|
|
||||||
const graph = await createProjectGraphAsync();
|
const graph = await createProjectGraphAsync();
|
||||||
@ -169,6 +176,7 @@ export function withNx(
|
|||||||
)(userWebpackConfig ? userWebpackConfig(a, b) : a, b);
|
)(userWebpackConfig ? userWebpackConfig(a, b) : a, b);
|
||||||
|
|
||||||
return nextConfig;
|
return nextConfig;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,9 @@
|
|||||||
import type { ExecutorContext } from '@nrwl/devkit';
|
import { ExecutorContext } from '@nrwl/devkit';
|
||||||
import {
|
|
||||||
applyChangesToString,
|
import { copyFileSync, existsSync } from 'fs';
|
||||||
ChangeType,
|
|
||||||
workspaceLayout,
|
|
||||||
workspaceRoot,
|
|
||||||
stripIndents,
|
|
||||||
} from '@nrwl/devkit';
|
|
||||||
import * as ts from 'typescript';
|
|
||||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import type { NextBuildBuilderOptions } from '../../../utils/types';
|
import type { NextBuildBuilderOptions } from '../../../utils/types';
|
||||||
import { findNodes } from 'nx/src/utils/typescript';
|
|
||||||
|
|
||||||
export function createNextConfigFile(
|
export function createNextConfigFile(
|
||||||
options: NextBuildBuilderOptions,
|
options: NextBuildBuilderOptions,
|
||||||
@ -21,53 +13,7 @@ export function createNextConfigFile(
|
|||||||
? join(context.root, options.nextConfig)
|
? join(context.root, options.nextConfig)
|
||||||
: join(context.root, options.root, 'next.config.js');
|
: join(context.root, options.root, 'next.config.js');
|
||||||
|
|
||||||
// Copy config file and our `with-nx.js` file to remove dependency on @nrwl/next for production build.
|
|
||||||
if (existsSync(nextConfigPath)) {
|
if (existsSync(nextConfigPath)) {
|
||||||
writeFileSync(join(options.outputPath, 'with-nx.js'), getWithNxContent());
|
copyFileSync(nextConfigPath, join(options.outputPath, 'next.config.js'));
|
||||||
writeFileSync(
|
|
||||||
join(options.outputPath, 'next.config.js'),
|
|
||||||
readFileSync(nextConfigPath)
|
|
||||||
.toString()
|
|
||||||
.replace('@nrwl/next/plugins/with-nx', './with-nx.js')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWithNxContent() {
|
|
||||||
const withNxFile = join(__dirname, '../../../../plugins/with-nx.js');
|
|
||||||
let withNxContent = readFileSync(withNxFile).toString();
|
|
||||||
const withNxSource = ts.createSourceFile(
|
|
||||||
withNxFile,
|
|
||||||
withNxContent,
|
|
||||||
ts.ScriptTarget.Latest,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
const getWithNxContextDeclaration = findNodes(
|
|
||||||
withNxSource,
|
|
||||||
ts.SyntaxKind.FunctionDeclaration
|
|
||||||
)?.find(
|
|
||||||
(node: ts.FunctionDeclaration) => node.name?.text === 'getWithNxContext'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (getWithNxContextDeclaration) {
|
|
||||||
withNxContent = applyChangesToString(withNxContent, [
|
|
||||||
{
|
|
||||||
type: ChangeType.Delete,
|
|
||||||
start: getWithNxContextDeclaration.getStart(withNxSource),
|
|
||||||
length: getWithNxContextDeclaration.getWidth(withNxSource),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: ChangeType.Insert,
|
|
||||||
index: getWithNxContextDeclaration.getStart(withNxSource),
|
|
||||||
text: stripIndents`function getWithNxContext() {
|
|
||||||
return {
|
|
||||||
workspaceRoot: '${workspaceRoot}',
|
|
||||||
libsDir: '${workspaceLayout().libsDir}'
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return withNxContent;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
import { cypressProjectGenerator } from '@nrwl/cypress';
|
import { ensurePackage, Tree } from '@nrwl/devkit';
|
||||||
import { Tree } from '@nrwl/devkit';
|
|
||||||
import { NormalizedSchema } from './normalize-options';
|
|
||||||
import { Linter } from '@nrwl/linter';
|
import { Linter } from '@nrwl/linter';
|
||||||
|
|
||||||
|
import { nxVersion } from '../../../utils/versions';
|
||||||
|
import { NormalizedSchema } from './normalize-options';
|
||||||
|
|
||||||
export async function addCypress(host: Tree, options: NormalizedSchema) {
|
export async function addCypress(host: Tree, options: NormalizedSchema) {
|
||||||
if (options.e2eTestRunner !== 'cypress') {
|
if (options.e2eTestRunner !== 'cypress') {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { cypressProjectGenerator } = ensurePackage<
|
||||||
|
typeof import('@nrwl/cypress')
|
||||||
|
>('@nrwl/cypress', nxVersion);
|
||||||
return cypressProjectGenerator(host, {
|
return cypressProjectGenerator(host, {
|
||||||
...options,
|
...options,
|
||||||
linter: Linter.EsLint,
|
linter: Linter.EsLint,
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
import { joinPathFragments, readJson, Tree, updateJson } from '@nrwl/devkit';
|
import {
|
||||||
import { jestProjectGenerator } from '@nrwl/jest';
|
ensurePackage,
|
||||||
|
joinPathFragments,
|
||||||
|
readJson,
|
||||||
|
Tree,
|
||||||
|
updateJson,
|
||||||
|
} from '@nrwl/devkit';
|
||||||
|
|
||||||
|
import { nxVersion } from '../../../utils/versions';
|
||||||
import { NormalizedSchema } from './normalize-options';
|
import { NormalizedSchema } from './normalize-options';
|
||||||
|
|
||||||
export async function addJest(host: Tree, options: NormalizedSchema) {
|
export async function addJest(host: Tree, options: NormalizedSchema) {
|
||||||
@ -7,6 +14,10 @@ export async function addJest(host: Tree, options: NormalizedSchema) {
|
|||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { jestProjectGenerator } = ensurePackage<typeof import('@nrwl/jest')>(
|
||||||
|
'@nrwl/jest',
|
||||||
|
nxVersion
|
||||||
|
);
|
||||||
const jestTask = await jestProjectGenerator(host, {
|
const jestTask = await jestProjectGenerator(host, {
|
||||||
...options,
|
...options,
|
||||||
project: options.projectName,
|
project: options.projectName,
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
addDependenciesToPackageJson,
|
addDependenciesToPackageJson,
|
||||||
convertNxGenerator,
|
convertNxGenerator,
|
||||||
|
ensurePackage,
|
||||||
GeneratorCallback,
|
GeneratorCallback,
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
Tree,
|
Tree,
|
||||||
} from '@nrwl/devkit';
|
} from '@nrwl/devkit';
|
||||||
|
|
||||||
import { jestInitGenerator } from '@nrwl/jest';
|
|
||||||
import { cypressInitGenerator } from '@nrwl/cypress';
|
|
||||||
import { reactDomVersion, reactVersion } from '@nrwl/react/src/utils/versions';
|
import { reactDomVersion, reactVersion } from '@nrwl/react/src/utils/versions';
|
||||||
import reactInitGenerator from '@nrwl/react/src/generators/init/init';
|
import reactInitGenerator from '@nrwl/react/src/generators/init/init';
|
||||||
import { initGenerator as jsInitGenerator } from '@nrwl/js';
|
import { initGenerator as jsInitGenerator } from '@nrwl/js';
|
||||||
@ -48,10 +47,17 @@ export async function nextInitGenerator(host: Tree, schema: InitSchema) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!schema.unitTestRunner || schema.unitTestRunner === 'jest') {
|
if (!schema.unitTestRunner || schema.unitTestRunner === 'jest') {
|
||||||
|
const { jestInitGenerator } = ensurePackage<typeof import('@nrwl/jest')>(
|
||||||
|
'@nrwl/jest',
|
||||||
|
nxVersion
|
||||||
|
);
|
||||||
const jestTask = await jestInitGenerator(host, schema);
|
const jestTask = await jestInitGenerator(host, schema);
|
||||||
tasks.push(jestTask);
|
tasks.push(jestTask);
|
||||||
}
|
}
|
||||||
if (!schema.e2eTestRunner || schema.e2eTestRunner === 'cypress') {
|
if (!schema.e2eTestRunner || schema.e2eTestRunner === 'cypress') {
|
||||||
|
const { cypressInitGenerator } = ensurePackage<
|
||||||
|
typeof import('@nrwl/cypress')
|
||||||
|
>('@nrwl/cypress', nxVersion);
|
||||||
const cypressTask = await cypressInitGenerator(host, {});
|
const cypressTask = await cypressInitGenerator(host, {});
|
||||||
tasks.push(cypressTask);
|
tasks.push(cypressTask);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { join, resolve } from 'path';
|
|||||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
|
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
|
||||||
import { Configuration, RuleSetRule } from 'webpack';
|
import { Configuration, RuleSetRule } from 'webpack';
|
||||||
import { FileReplacement } from './types';
|
import { FileReplacement } from './types';
|
||||||
import { createCopyPlugin, normalizeAssets } from '@nrwl/webpack';
|
import { createCopyPlugin } from './create-copy-plugin';
|
||||||
import {
|
import {
|
||||||
createTmpTsConfig,
|
createTmpTsConfig,
|
||||||
DependentBuildableProjectNode,
|
DependentBuildableProjectNode,
|
||||||
@ -77,14 +77,7 @@ export function createWebpackConfig(
|
|||||||
|
|
||||||
// Copy (shared) assets to `public` folder during client-side compilation
|
// Copy (shared) assets to `public` folder during client-side compilation
|
||||||
if (!isServer && Array.isArray(assets) && assets.length > 0) {
|
if (!isServer && Array.isArray(assets) && assets.length > 0) {
|
||||||
config.plugins.push(
|
config.plugins.push(createCopyPlugin(assets, workspaceRoot, projectRoot));
|
||||||
createCopyPlugin(
|
|
||||||
normalizeAssets(assets, workspaceRoot, projectRoot).map((asset) => ({
|
|
||||||
...asset,
|
|
||||||
output: join('../public', asset.output),
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
83
packages/next/src/utils/create-copy-plugin.ts
Normal file
83
packages/next/src/utils/create-copy-plugin.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import * as CopyWebpackPlugin from 'copy-webpack-plugin';
|
||||||
|
import { normalizePath } from 'nx/src/utils/path';
|
||||||
|
import { basename, dirname, join, relative, resolve } from 'path';
|
||||||
|
import { statSync } from 'fs';
|
||||||
|
|
||||||
|
interface AssetGlobPattern {
|
||||||
|
glob: string;
|
||||||
|
input: string;
|
||||||
|
output: string;
|
||||||
|
ignore?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAssets(
|
||||||
|
assets: any[],
|
||||||
|
root: string,
|
||||||
|
sourceRoot: string
|
||||||
|
): AssetGlobPattern[] {
|
||||||
|
return assets.map((asset) => {
|
||||||
|
if (typeof asset === 'string') {
|
||||||
|
const assetPath = normalizePath(asset);
|
||||||
|
const resolvedAssetPath = resolve(root, assetPath);
|
||||||
|
const resolvedSourceRoot = resolve(root, sourceRoot);
|
||||||
|
|
||||||
|
if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) {
|
||||||
|
throw new Error(
|
||||||
|
`The ${resolvedAssetPath} asset path must start with the project source root: ${sourceRoot}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDirectory = statSync(resolvedAssetPath).isDirectory();
|
||||||
|
const input = isDirectory
|
||||||
|
? resolvedAssetPath
|
||||||
|
: dirname(resolvedAssetPath);
|
||||||
|
const output = relative(resolvedSourceRoot, resolve(root, input));
|
||||||
|
const glob = isDirectory ? '**/*' : basename(resolvedAssetPath);
|
||||||
|
return {
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
glob,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (asset.output.startsWith('..')) {
|
||||||
|
throw new Error(
|
||||||
|
'An asset cannot be written to a location outside of the output path.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetPath = normalizePath(asset.input);
|
||||||
|
const resolvedAssetPath = resolve(root, assetPath);
|
||||||
|
return {
|
||||||
|
...asset,
|
||||||
|
input: resolvedAssetPath,
|
||||||
|
// Now we remove starting slash to make Webpack place it from the output root.
|
||||||
|
output: asset.output.replace(/^\//, ''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCopyPlugin(
|
||||||
|
assets: any[],
|
||||||
|
root: string,
|
||||||
|
sourceRoot: string
|
||||||
|
) {
|
||||||
|
return new CopyWebpackPlugin({
|
||||||
|
patterns: normalizeAssets(assets, root, sourceRoot).map((asset) => {
|
||||||
|
return {
|
||||||
|
context: asset.input,
|
||||||
|
to: join('../public', asset.output),
|
||||||
|
from: asset.glob,
|
||||||
|
globOptions: {
|
||||||
|
ignore: [
|
||||||
|
'.gitkeep',
|
||||||
|
'**/.DS_Store',
|
||||||
|
'**/Thumbs.db',
|
||||||
|
...(asset.ignore ?? []),
|
||||||
|
],
|
||||||
|
dot: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user