@apployees-nx/webserver
Version:
A create-react-app inspired plugin for Nx, with SSR and PWA capabilities.
327 lines (292 loc) • 10.7 kB
text/typescript
/*******************************************************************************
* © Apployees Inc., 2019
* All Rights Reserved.
******************************************************************************/
import ts from "typescript";
import {
apply,
chain,
externalSchematic,
MergeStrategy,
mergeWith,
move,
noop,
Rule,
SchematicContext,
template,
Tree,
url,
} from "@angular-devkit/schematics";
import { join, normalize, Path } from "@angular-devkit/core";
import { Schema } from "./schema";
import {
addLintFiles,
findNodes,
generateProjectLint,
getNpmScope,
insert,
offsetFromRoot,
toFileName,
updateJsonInTree,
updateWorkspaceInTree,
} from "@nrwl/workspace";
import init from "../init/init";
import { existsSync } from "fs";
import path from "path";
import { InsertChange } from "@nrwl/workspace/src/utils/ast-utils";
interface INormalizedSchema extends Schema {
appProjectRoot: Path;
parsedTags: string[];
}
function updateNxJson(options: INormalizedSchema): Rule {
return updateJsonInTree(`/nx.json`, (json) => {
return {
...json,
projects: {
...json.projects,
[options.name]: { tags: options.parsedTags },
},
};
});
}
function getBuildConfig(project: any, options: INormalizedSchema) {
return {
builder: "@apployees-nx/webserver:build",
options: {
outputPath: join(normalize("dist"), options.appProjectRoot),
appHtml: join(project.sourceRoot, "public", "app.html"),
serverMain: join(project.sourceRoot, "index.ts"),
clientMain: join(project.sourceRoot, "client", "index.tsx"),
favicon: join(project.sourceRoot, "public", "logo512.png"),
manifestJson: join(project.sourceRoot, "public", "manifest.json"),
clientOtherEntries: {
// eslint-disable-next-line @typescript-eslint/camelcase
anotherClientEntry_head: join(project.sourceRoot, "client", "anotherClientEntry.ts"),
},
clientWebpackConfig: join(options.appProjectRoot, "webpack.client.overrides.js"),
lessStyleVariables: join(options.appProjectRoot, "antd-theme.less"),
tsConfig: join(options.appProjectRoot, "tsconfig.app.json"),
assets: [join(project.sourceRoot, "public")],
},
configurations: {
production: {
extractLicenses: true,
inspect: false,
watch: false,
dev: false,
fileReplacements: [
{
replace: join(project.sourceRoot, "environments/environment.ts"),
with: join(project.sourceRoot, "environments/environment.prod.ts"),
},
],
},
development: {
dev: true,
inspect: true,
extractLicenses: false,
notifier: {
excludeWarnings: true,
},
},
},
};
}
function updateWorkspaceJson(options: INormalizedSchema): Rule {
return updateWorkspaceInTree((workspaceJson) => {
const project = {
root: options.appProjectRoot,
sourceRoot: join(options.appProjectRoot, "src"),
projectType: "application",
prefix: options.name,
schematics: {},
architect: {} as any,
};
project.architect.build = getBuildConfig(project, options);
project.architect.lint = generateProjectLint(
normalize(project.root),
join(normalize(project.root), "tsconfig.app.json"),
options.linter,
);
workspaceJson.projects[options.name] = project;
workspaceJson.defaultProject = workspaceJson.defaultProject || options.name;
return workspaceJson;
});
}
function addAppFiles(options: INormalizedSchema, npmScope: string): Rule {
let appDir = path.resolve(__dirname, path.normalize(`schematics/application/files/app`));
if (!existsSync(appDir)) {
appDir = path.resolve(__dirname, path.normalize(`files/app`));
}
return mergeWith(
apply(url(appDir), [
template({
tmpl: "",
name: options.name,
npmScope: npmScope,
root: options.appProjectRoot,
offset: offsetFromRoot(options.appProjectRoot),
}),
move(options.appProjectRoot),
]),
);
}
function addWorkspaceFiles(options: INormalizedSchema, npmScope: string): Rule {
let workspaceFilesDir = path.resolve(__dirname, path.normalize(`schematics/application/workspace-files`));
if (!existsSync(workspaceFilesDir)) {
workspaceFilesDir = path.resolve(__dirname, path.normalize(`workspace-files`));
}
return (host: Tree, context: SchematicContext) => {
if (
!host.exists(path.join("config", "jest", "cssTransform.js")) &&
!host.exists(path.join("config", "jest", "fileTransform.js"))
) {
return mergeWith(
apply(url(workspaceFilesDir), [
template({
tmpl: "",
name: options.name,
npmScope: npmScope,
}),
move(`/`),
]),
);
}
};
}
function modifyRootJestConfig(options: INormalizedSchema, npmScope: string): Rule {
return (host: Tree, context: SchematicContext) => {
const jestConfigFilePath = `/jest.config.js`;
const cssTransformLine = `\t'^.+\\.(css|sass|scss|less)$': \`\${__dirname}/config/jest/cssTransform.js\`,`;
const fileTransformLine = `\t'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': \`\${__dirname}/config/jest/fileTransform.js\``;
const otherLines = `,
setupFiles: [
"react-app-polyfill/jsdom"
],
testEnvironment: "jest-environment-jsdom-fourteen"`;
if (host.exists(jestConfigFilePath)) {
const jestConfigSource = host.read(jestConfigFilePath)!.toString("utf-8");
if (jestConfigSource.indexOf("react-app-polyfill/jsdom") >= 0) {
// already added.
return;
}
let insertTransformChange, insertOtherLinesChange;
const jestConfigSourceFile = ts.createSourceFile(
jestConfigFilePath,
jestConfigSource,
ts.ScriptTarget.Latest,
true,
);
const propertyAssignments = findNodes(jestConfigSourceFile, ts.SyntaxKind.PropertyAssignment);
if (propertyAssignments && propertyAssignments.length > 0) {
for (const propAssignment of propertyAssignments) {
const firstChild = findNodes(propAssignment, ts.SyntaxKind.Identifier, 1);
const secondChild = findNodes(propAssignment, ts.SyntaxKind.ObjectLiteralExpression, 1);
/**
* For:
*
* transform: { <-- identifier "transform" and ObjectLiteralExpression value
* ... <-- PropertyAssignments
* }
*/
if (
firstChild &&
firstChild.length > 0 &&
firstChild[0].getFullText(jestConfigSourceFile).indexOf("transform") >= 0 &&
secondChild &&
secondChild.length > 0
) {
// add transform lines after the last child in the value
const maybePropertyAssignment = findNodes(secondChild[0], ts.SyntaxKind.PropertyAssignment, 1);
if (maybePropertyAssignment && maybePropertyAssignment.length > 0) {
insertTransformChange = new InsertChange(
jestConfigFilePath,
maybePropertyAssignment[0].getEnd(),
`,\n${cssTransformLine}\n${fileTransformLine}`,
);
// add the other lines after the transform line.
insertOtherLinesChange = new InsertChange(jestConfigFilePath, propAssignment.getEnd(), otherLines);
break;
}
}
}
}
if (!insertTransformChange) {
const transformChanges = `\n\n/* Add the following lines to your "transform" object in your jest config.\n\n${cssTransformLine},\n${fileTransformLine}\n\n\nAdd the following next to your transform (root level of the exported jest config object):\n\n${otherLines}\n\n*/`;
insertTransformChange = new InsertChange(jestConfigFilePath, jestConfigSource.length, transformChanges);
context.logger.warn(
"Please see your jest.config.js file in the root of the project to make some necessary changes.",
);
}
insert(host, jestConfigFilePath, [insertTransformChange, insertOtherLinesChange].filter(Boolean));
} else {
return updateJsonInTree(`/package.json`, (json) => {
if (!json.jest) {
return json;
}
let transform = json.jest.transform;
if (!transform) {
transform = {};
json.jest.transform = transform;
}
transform[`^.+\\.(css|sass|scss|less)$`] = `config/jest/cssTransform.js`;
transform[`^(?!.*\\.(js|jsx|ts|tsx|css|json)$)`] = `config/jest/fileTransform.js`;
return json;
});
}
};
}
function updateRootPackageJson(options: INormalizedSchema): Rule {
return (host: Tree) => {
return updateJsonInTree(`/package.json`, (json) => {
if (!json.scripts) {
json.scripts = {};
}
json.scripts[`dev-${options.name}`] = `nx build ${options.name} --configuration development`;
json.scripts[`build-${options.name}`] = `nx build ${options.name} --configuration production`;
json.scripts[`lint-${options.name}`] = `nx lint ${options.name}`;
json.scripts[`test-${options.name}`] = `nx test ${options.name}`;
return json;
});
};
}
export default function (schema: Schema): Rule {
return (host: Tree, context: SchematicContext) => {
const options = normalizeOptions(schema);
const npmScope = getNpmScope(host) || "yourOrg";
return chain([
init({
skipFormat: true,
}),
addLintFiles(options.appProjectRoot, options.linter),
addAppFiles(options, npmScope),
addWorkspaceFiles(options, npmScope),
modifyRootJestConfig(options, npmScope),
updateWorkspaceJson(options),
updateNxJson(options),
updateRootPackageJson(options),
options.unitTestRunner === "jest"
? externalSchematic("@nrwl/jest", "jest-project", {
project: options.name,
setupFile: "none",
supportTsx: true,
skipSerializers: true,
})
: noop(),
])(host, context);
};
}
function normalizeOptions(options: Schema): INormalizedSchema {
const appDirectory = options.directory
? `${toFileName(options.directory)}/${toFileName(options.name)}`
: toFileName(options.name);
const appProjectName = appDirectory.replace(new RegExp("/", "g"), "-");
const appProjectRoot = join(normalize("apps"), appDirectory);
const parsedTags = options.tags ? options.tags.split(",").map((s) => s.trim()) : [];
return {
...options,
name: toFileName(appProjectName),
appProjectRoot,
parsedTags,
};
}