Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(): validate configuration after loaded #1427

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions actions/add.action.ts
Expand Up @@ -69,7 +69,7 @@ export class AddAction extends AbstractAction {

for (const property in configurationProjects) {
if (
configurationProjects[property].sourceRoot ===
configurationProjects[property]!.sourceRoot ===
configuration.sourceRoot
) {
defaultProjectName = property + defaultLabel;
Expand All @@ -89,7 +89,7 @@ export class AddAction extends AbstractAction {
);
const project = answers.appName.replace(defaultLabel, '');
if (project !== configuration.sourceRoot) {
sourceRoot = configurationProjects[project].sourceRoot;
sourceRoot = configurationProjects[project]!.sourceRoot;
}
}
return { name: 'sourceRoot', value: sourceRoot };
Expand Down
23 changes: 7 additions & 16 deletions actions/build.action.ts
Expand Up @@ -11,15 +11,11 @@ import { TypeScriptBinaryLoader } from '../lib/compiler/typescript-loader';
import { WatchCompiler } from '../lib/compiler/watch-compiler';
import { WebpackCompiler } from '../lib/compiler/webpack-compiler';
import { WorkspaceUtils } from '../lib/compiler/workspace-utils';
import {
ConfigurationLoader,
NestConfigurationLoader,
} from '../lib/configuration';
import { defaultOutDir } from '../lib/configuration/defaults';
import { FileSystemReader } from '../lib/readers';
import { ERROR_PREFIX } from '../lib/ui';
import { AbstractAction } from './abstract.action';
import webpack = require('webpack');
import { loadConfiguration } from '../lib/utils/load-configuration';

export class BuildAction extends AbstractAction {
protected readonly pluginsLoader = new PluginsLoader();
Expand All @@ -36,10 +32,6 @@ export class BuildAction extends AbstractAction {
this.tsConfigProvider,
this.tsLoader,
);
protected readonly fileSystemReader = new FileSystemReader(process.cwd());
protected readonly loader: ConfigurationLoader = new NestConfigurationLoader(
this.fileSystemReader,
);
protected readonly assetsManager = new AssetsManager();
protected readonly workspaceUtils = new WorkspaceUtils();

Expand Down Expand Up @@ -73,9 +65,9 @@ export class BuildAction extends AbstractAction {
isDebugEnabled = false,
onSuccess?: () => void,
) {
const configFileName = options.find((option) => option.name === 'config')!
.value as string;
const configuration = await this.loader.load(configFileName);
const configFileName = options.find((option) => option.name === 'config')
?.value as string | undefined;
const configuration = await loadConfiguration(configFileName);
const appName = inputs.find((input) => input.name === 'app')!
.value as string;

Expand All @@ -86,9 +78,8 @@ export class BuildAction extends AbstractAction {
'path',
options,
);
const { options: tsOptions } = this.tsConfigProvider.getByConfigFilename(
pathToTsconfig,
);
const { options: tsOptions } =
this.tsConfigProvider.getByConfigFilename(pathToTsconfig);
const outDir = tsOptions.outDir || defaultOutDir;
const isWebpackEnabled = getValueOrDefault<boolean>(
configuration,
Expand Down Expand Up @@ -162,7 +153,7 @@ export class BuildAction extends AbstractAction {
): (
config: webpack.Configuration,
webpackRef: typeof webpack,
) => webpack.Configuration | webpack.Configuration {
) => webpack.Configuration {
const pathToWebpackFile = join(process.cwd(), webpackPath);
try {
return require(pathToWebpackFile);
Expand Down
4 changes: 2 additions & 2 deletions actions/generate.action.ts
Expand Up @@ -66,7 +66,7 @@ const generateFiles = async (inputs: Input[]) => {

for (const property in configurationProjects) {
if (
configurationProjects[property].sourceRoot === configuration.sourceRoot
configurationProjects[property]!.sourceRoot === configuration.sourceRoot
) {
defaultProjectName = property + defaultLabel;
break;
Expand All @@ -86,7 +86,7 @@ const generateFiles = async (inputs: Input[]) => {

const project: string = answers.appName.replace(defaultLabel, '');
if (project !== configuration.sourceRoot) {
sourceRoot = configurationProjects[project].sourceRoot;
sourceRoot = configurationProjects[project]!.sourceRoot;
}

if (answers.appName !== defaultProjectName) {
Expand Down
7 changes: 4 additions & 3 deletions actions/start.action.ts
Expand Up @@ -10,13 +10,14 @@ import { Configuration } from '../lib/configuration';
import { defaultOutDir } from '../lib/configuration/defaults';
import { ERROR_PREFIX } from '../lib/ui';
import { BuildAction } from './build.action';
import { loadConfiguration } from '../lib/utils/load-configuration';

export class StartAction extends BuildAction {
public async handle(inputs: Input[], options: Input[]) {
try {
const configFileName = options.find((option) => option.name === 'config')!
.value as string;
const configuration = await this.loader.load(configFileName);
const configFileName = options.find((option) => option.name === 'config')
?.value as string | undefined;
const configuration = await loadConfiguration(configFileName);
const appName = inputs.find((input) => input.name === 'app')!
.value as string;

Expand Down
3 changes: 1 addition & 2 deletions lib/compiler/defaults/webpack-defaults.ts
@@ -1,7 +1,6 @@
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
import { join } from 'path';
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
import { defaultConfiguration } from '../../configuration/defaults';
import { appendTsExtension } from '../helpers/append-extension';
import { MultiNestCompilerPlugins } from '../plugins-loader';
import webpack = require('webpack');
Expand All @@ -12,7 +11,7 @@ export const webpackDefaultsFactory = (
relativeSourceRoot: string,
entryFilename: string,
isDebugEnabled = false,
tsConfigFile = defaultConfiguration.compilerOptions.tsConfigPath,
tsConfigFile: string,
plugins: MultiNestCompilerPlugins,
): webpack.Configuration => ({
entry: appendTsExtension(join(sourceRoot, entryFilename)),
Expand Down
101 changes: 66 additions & 35 deletions lib/configuration/configuration.ts
@@ -1,4 +1,9 @@
import * as Joi from 'types-joi';
import { InterfaceFrom } from 'types-joi';
import { Schema } from 'joi';

export type Asset = 'string' | AssetEntry;

export interface AssetEntry {
glob: string;
include?: string;
Expand All @@ -16,42 +21,68 @@ export interface ActionOnFile {
watchAssetsMode: boolean;
}

interface CompilerOptions {
tsConfigPath?: string;
webpack?: boolean;
webpackConfigPath?: string;
plugins?: string[] | PluginOptions[];
assets?: string[];
deleteOutDir?: boolean;
}
const pluginOptionsSchema = Joi.object({
name: Joi.string().required(),
options: Joi.object().pattern(Joi.string(), Joi.any()).required(),
});

interface PluginOptions {
name: string;
options: Record<string, any>[];
}
const generateOptionsSchema = Joi.object({
spec: Joi.alternatives([
Joi.boolean(),
Joi.object().pattern(Joi.string(), Joi.boolean().required()),
]).optional(),
});

interface GenerateOptions {
spec?: boolean | Record<string, boolean>;
}
const compilerOptionsSchema = Joi.object({
tsConfigPath: Joi.string().default('tsconfig.build.json'),
webpack: Joi.boolean().default(false),
webpackConfigPath: Joi.string().default('webpack.config.js'),
plugins: Joi.array()
.items(Joi.alternatives([Joi.string(), pluginOptionsSchema]))
.default([]),
assets: Joi.array()
.items(
Joi.alternatives([
Joi.string(),
Joi.object({
include: Joi.string().optional(),
exclude: Joi.string().optional(),
outDir: Joi.string().optional(),
watchAssets: Joi.boolean().optional(),
}),
]),
)
.default([]),
watchAssets: Joi.boolean().default(false),
deleteOutDir: Joi.boolean().default(false),
});

export interface ProjectConfiguration {
type?: string;
root?: string;
entryFile?: string;
sourceRoot?: string;
compilerOptions?: CompilerOptions;
}
const projectConfigurationSchema = Joi.object({
type: Joi.string().optional(),
root: Joi.string().optional(),
entryFile: Joi.string().optional(),
sourceRoot: Joi.string().optional(),
compilerOptions: compilerOptionsSchema.optional(),
});

export interface Configuration {
[key: string]: any;
language?: string;
collection?: string;
sourceRoot?: string;
entryFile?: string;
monorepo?: boolean;
compilerOptions?: CompilerOptions;
generateOptions?: GenerateOptions;
projects?: {
[key: string]: ProjectConfiguration;
};
}
const configurationSchemaOfTypesJoi = Joi.object({
language: Joi.string().default('ts'),
collection: Joi.string().default('@nestjs/schematics'),
sourceRoot: Joi.string().default('src'),
entryFile: Joi.string().default('main'),
monorepo: Joi.boolean().default(false),
compilerOptions: compilerOptionsSchema.default(undefined),
generateOptions: generateOptionsSchema.default(undefined),
projects: Joi.object()
.pattern(Joi.string(), projectConfigurationSchema.required())
.default({}),
}).required();

export const configurationSchema =
configurationSchemaOfTypesJoi as unknown as Schema;

export type Configuration = InterfaceFrom<typeof configurationSchemaOfTypesJoi>;

export type ProjectConfiguration = InterfaceFrom<
typeof projectConfigurationSchema
>;
19 changes: 0 additions & 19 deletions lib/configuration/defaults.ts
@@ -1,22 +1,3 @@
import { Configuration } from './configuration';

export const defaultConfiguration: Required<Configuration> = {
language: 'ts',
sourceRoot: 'src',
collection: '@nestjs/schematics',
entryFile: 'main',
projects: {},
monorepo: false,
compilerOptions: {
tsConfigPath: 'tsconfig.build.json',
webpack: false,
webpackConfigPath: 'webpack.config.js',
plugins: [],
assets: [],
},
generateOptions: {},
};

export const defaultOutDir = 'dist';
export const defaultGitIgnore = `# compiled output
/dist
Expand Down
49 changes: 21 additions & 28 deletions lib/configuration/nest-configuration.loader.ts
@@ -1,38 +1,31 @@
import { Reader } from '../readers';
import { Configuration } from './configuration';
import { ConfigurationLoader } from './configuration.loader';
import { defaultConfiguration } from './defaults';
import { Schema } from 'joi';

export class NestConfigurationLoader implements ConfigurationLoader {
constructor(private readonly reader: Reader) {}
constructor(
private readonly reader: Reader,
private readonly configurationSchema: Schema<any>,
) {}

public async load(name?: string): Promise<Required<Configuration>> {
const content: string | undefined = name
? await this.reader.read(name)
: await this.reader.readAnyOf([
'.nestcli.json',
'.nest-cli.json',
'nest-cli.json',
'nest.json',
]);
private validateConfiguration(configuration: Configuration): Configuration {
const { error, value } = this.configurationSchema.validate(configuration);
if (error) throw error;
return value;
}

if (!content) {
return defaultConfiguration;
}
public async load(name?: string): Promise<Required<Configuration>> {
const content =
(name
? await this.reader.read(name)
: await this.reader.readAnyOf([
'.nestcli.json',
'.nest-cli.json',
'nest-cli.json',
'nest.json',
])) || '{}';
const fileConfig = JSON.parse(content);
if (fileConfig.compilerOptions) {
return {
...defaultConfiguration,
...fileConfig,
compilerOptions: {
...defaultConfiguration.compilerOptions,
...fileConfig.compilerOptions,
},
};
}
return {
...defaultConfiguration,
...fileConfig,
};
return this.validateConfiguration(fileConfig);
}
}
15 changes: 11 additions & 4 deletions lib/utils/load-configuration.ts
@@ -1,10 +1,17 @@
import { Configuration, ConfigurationLoader } from '../configuration';
import { NestConfigurationLoader } from '../configuration/nest-configuration.loader';
import {
Configuration,
ConfigurationLoader,
configurationSchema,
NestConfigurationLoader,
} from '../configuration';
import { FileSystemReader } from '../readers';

export async function loadConfiguration(): Promise<Required<Configuration>> {
export async function loadConfiguration(
name?: string,
): Promise<Required<Configuration>> {
const loader: ConfigurationLoader = new NestConfigurationLoader(
new FileSystemReader(process.cwd()),
configurationSchema,
);
return loader.load();
return loader.load(name);
}