mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
321 lines
15 KiB
321 lines
15 KiB
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.nxComponentTestingPreset = nxComponentTestingPreset;
|
|
const cypress_preset_1 = require("@nx/cypress/plugins/cypress-preset");
|
|
const ct_helpers_1 = require("@nx/cypress/src/utils/ct-helpers");
|
|
const devkit_1 = require("@nx/devkit");
|
|
const ts_solution_setup_1 = require("@nx/js/src/utils/typescript/ts-solution-setup");
|
|
const fs_1 = require("fs");
|
|
const path_1 = require("path");
|
|
const semver_1 = require("semver");
|
|
/**
|
|
* Angular nx preset for Cypress Component Testing
|
|
*
|
|
* This preset contains the base configuration
|
|
* for your component tests that nx recommends.
|
|
* including a devServer that supports nx workspaces.
|
|
* you can easily extend this within your cypress config via spreading the preset
|
|
* @example
|
|
* export default defineConfig({
|
|
* component: {
|
|
* ...nxComponentTestingPreset(__filename)
|
|
* // add your own config here
|
|
* }
|
|
* })
|
|
*
|
|
* @param pathToConfig will be used for loading project options and to construct the output paths for videos and screenshots
|
|
* @param options override options
|
|
*/
|
|
function nxComponentTestingPreset(pathToConfig, options) {
|
|
if (global.NX_GRAPH_CREATION) {
|
|
// this is only used by plugins, so we don't need the component testing
|
|
// options, cast to any to avoid type errors
|
|
return (0, cypress_preset_1.nxBaseCypressPreset)(pathToConfig, {
|
|
testingType: 'component',
|
|
});
|
|
}
|
|
let graph;
|
|
try {
|
|
graph = (0, devkit_1.readCachedProjectGraph)();
|
|
}
|
|
catch (e) {
|
|
throw new Error(
|
|
// don't want to strip indents so error stack has correct indentation
|
|
`Unable to read the project graph for component testing.
|
|
This is likely due to not running via nx. i.e. 'nx component-test my-project'.
|
|
Please open an issue if this error persists.
|
|
${e.stack ? e.stack : e}`);
|
|
}
|
|
const ctProjectConfig = (0, ct_helpers_1.getProjectConfigByPath)(graph, pathToConfig);
|
|
const ctConfigurationName = process.env.NX_CYPRESS_TARGET_CONFIGURATION;
|
|
const ctContext = (0, ct_helpers_1.createExecutorContext)(graph, ctProjectConfig.targets, ctProjectConfig.name, options?.ctTargetName || 'component-test', ctConfigurationName);
|
|
const buildTarget = options?.buildTarget
|
|
? (0, devkit_1.parseTargetString)(options.buildTarget, graph)
|
|
: // for backwards compat, if no buildTargetin the preset options, get it from the target options
|
|
getBuildableTarget(ctContext);
|
|
if (!buildTarget.project && !graph.nodes?.[buildTarget.project]?.data) {
|
|
throw new Error((0, devkit_1.stripIndents) `Unable to find project configuration for build target.
|
|
Project Name? ${buildTarget.project}
|
|
Has project config? ${!!graph.nodes?.[buildTarget.project]?.data}`);
|
|
}
|
|
const fromWorkspaceRoot = (0, path_1.relative)(ctContext.root, pathToConfig);
|
|
const normalizedFromWorkspaceRootPath = (0, fs_1.lstatSync)(pathToConfig).isFile()
|
|
? (0, path_1.dirname)(fromWorkspaceRoot)
|
|
: fromWorkspaceRoot;
|
|
const offset = isOffsetNeeded(ctContext, ctProjectConfig)
|
|
? (0, devkit_1.offsetFromRoot)(normalizedFromWorkspaceRootPath)
|
|
: undefined;
|
|
const buildContext = (0, ct_helpers_1.createExecutorContext)(graph, graph.nodes[buildTarget.project]?.data.targets, buildTarget.project, buildTarget.target, buildTarget.configuration);
|
|
const buildableProjectConfig = normalizeBuildTargetOptions(buildContext, ctContext, offset);
|
|
return {
|
|
...(0, cypress_preset_1.nxBaseCypressPreset)(pathToConfig, { testingType: 'component' }),
|
|
// NOTE: cannot use a glob pattern since it will break cypress generated tsconfig.
|
|
specPattern: ['src/**/*.cy.ts', 'src/**/*.cy.js'],
|
|
// Cy v12.17.0+ does not work with aboslute paths for index file
|
|
// but does with relative pathing, since relative path is the default location, we can omit it
|
|
indexHtmlFile: requiresAbsolutePath()
|
|
? (0, devkit_1.joinPathFragments)(ctContext.root, ctProjectConfig.root, 'cypress', 'support', 'component-index.html')
|
|
: undefined,
|
|
devServer: {
|
|
// cypress uses string union type,
|
|
// need to use const to prevent typing to string
|
|
...{
|
|
framework: 'angular',
|
|
bundler: 'webpack',
|
|
},
|
|
options: {
|
|
projectConfig: buildableProjectConfig,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
function getBuildableTarget(ctContext) {
|
|
const targets = ctContext.projectGraph.nodes[ctContext.projectName]?.data?.targets;
|
|
const targetConfig = targets?.[ctContext.targetName];
|
|
if (!targetConfig) {
|
|
throw new Error((0, devkit_1.stripIndents) `Unable to find component testing target configuration in project '${ctContext.projectName}'.
|
|
Has targets? ${!!targets}
|
|
Has target name? ${ctContext.targetName}
|
|
Has ct project name? ${ctContext.projectName}
|
|
`);
|
|
}
|
|
const cypressCtOptions = (0, devkit_1.readTargetOptions)({
|
|
project: ctContext.projectName,
|
|
target: ctContext.targetName,
|
|
configuration: ctContext.configurationName,
|
|
}, ctContext);
|
|
if (!cypressCtOptions.devServerTarget) {
|
|
throw new Error(`Unable to find the 'devServerTarget' executor option in the '${ctContext.targetName}' target of the '${ctContext.projectName}' project`);
|
|
}
|
|
return (0, devkit_1.parseTargetString)(cypressCtOptions.devServerTarget, ctContext.projectGraph);
|
|
}
|
|
function normalizeBuildTargetOptions(buildContext, ctContext, offset) {
|
|
const options = (0, devkit_1.readTargetOptions)({
|
|
project: buildContext.projectName,
|
|
target: buildContext.targetName,
|
|
configuration: buildContext.configurationName,
|
|
}, buildContext);
|
|
const project = buildContext.projectsConfigurations.projects[buildContext.projectName];
|
|
const sourceRoot = (0, ts_solution_setup_1.getProjectSourceRoot)(project);
|
|
const buildOptions = withSchemaDefaults(options, sourceRoot, buildContext.root);
|
|
// cypress creates a tsconfig if one isn't preset
|
|
// that contains all the support required for angular and component tests
|
|
delete buildOptions.tsConfig;
|
|
if (offset) {
|
|
// polyfill entries might be local files or files that are resolved from node_modules
|
|
// like zone.js.
|
|
// prevents error from webpack saying can't find <offset>/zone.js.
|
|
const handlePolyfillPath = (polyfill) => {
|
|
const maybeFullPath = (0, path_1.join)(ctContext.root, polyfill.split('/').join(path_1.sep));
|
|
if ((0, fs_1.existsSync)(maybeFullPath)) {
|
|
return (0, devkit_1.joinPathFragments)(offset, polyfill);
|
|
}
|
|
return polyfill;
|
|
};
|
|
// paths need to be unix paths for angular devkit
|
|
if (buildOptions.polyfills) {
|
|
buildOptions.polyfills =
|
|
Array.isArray(buildOptions.polyfills) &&
|
|
buildOptions.polyfills.length > 0
|
|
? buildOptions.polyfills.map((p) => handlePolyfillPath(p))
|
|
: handlePolyfillPath(buildOptions.polyfills);
|
|
}
|
|
buildOptions.main = (0, devkit_1.joinPathFragments)(offset, buildOptions.main);
|
|
buildOptions.index =
|
|
typeof buildOptions.index === 'string'
|
|
? (0, devkit_1.joinPathFragments)(offset, buildOptions.index)
|
|
: {
|
|
...buildOptions.index,
|
|
input: (0, devkit_1.joinPathFragments)(offset, buildOptions.index.input),
|
|
};
|
|
buildOptions.fileReplacements = buildOptions.fileReplacements.map((fr) => {
|
|
fr.replace = (0, devkit_1.joinPathFragments)(offset, fr.replace);
|
|
fr.with = (0, devkit_1.joinPathFragments)(offset, fr.with);
|
|
return fr;
|
|
});
|
|
}
|
|
// if the ct project isn't being used in the build project
|
|
// then we don't want to have the assets/scripts/styles be included to
|
|
// prevent inclusion of unintended stuff like tailwind
|
|
if (buildContext.projectName === ctContext.projectName ||
|
|
(0, ct_helpers_1.isCtProjectUsingBuildProject)(ctContext.projectGraph, buildContext.projectName, ctContext.projectName)) {
|
|
if (offset) {
|
|
buildOptions.assets = buildOptions.assets.map((asset) => {
|
|
return typeof asset === 'string'
|
|
? (0, devkit_1.joinPathFragments)(offset, asset)
|
|
: { ...asset, input: (0, devkit_1.joinPathFragments)(offset, asset.input) };
|
|
});
|
|
buildOptions.styles = buildOptions.styles.map((style) => {
|
|
return typeof style === 'string'
|
|
? (0, devkit_1.joinPathFragments)(offset, style)
|
|
: { ...style, input: (0, devkit_1.joinPathFragments)(offset, style.input) };
|
|
});
|
|
buildOptions.scripts = buildOptions.scripts.map((script) => {
|
|
return typeof script === 'string'
|
|
? (0, devkit_1.joinPathFragments)(offset, script)
|
|
: { ...script, input: (0, devkit_1.joinPathFragments)(offset, script.input) };
|
|
});
|
|
if (buildOptions.stylePreprocessorOptions?.includePaths.length > 0) {
|
|
buildOptions.stylePreprocessorOptions = {
|
|
includePaths: buildOptions.stylePreprocessorOptions.includePaths.map((path) => {
|
|
return (0, devkit_1.joinPathFragments)(offset, path);
|
|
}),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
const stylePath = getTempStylesForTailwind(ctContext);
|
|
buildOptions.styles = stylePath ? [stylePath] : [];
|
|
buildOptions.assets = [];
|
|
buildOptions.scripts = [];
|
|
buildOptions.stylePreprocessorOptions = { includePaths: [] };
|
|
}
|
|
return {
|
|
root: offset ? (0, devkit_1.joinPathFragments)(offset, project.root) : project.root,
|
|
sourceRoot: offset ? (0, devkit_1.joinPathFragments)(offset, sourceRoot) : sourceRoot,
|
|
buildOptions: {
|
|
...buildOptions,
|
|
// this property is only valid for cy v12.9.0+
|
|
workspaceRoot: offset ? undefined : ctContext.root,
|
|
},
|
|
};
|
|
}
|
|
function withSchemaDefaults(options, sourceRoot, workspaceRoot) {
|
|
if (!options.main && !options.browser) {
|
|
options.browser = (0, devkit_1.joinPathFragments)(sourceRoot, 'main.ts');
|
|
if (!(0, fs_1.existsSync)((0, path_1.join)(workspaceRoot, options.browser))) {
|
|
throw new Error('Missing executor options "main" and "browser"');
|
|
}
|
|
}
|
|
if (!options.index) {
|
|
throw new Error('Missing executor options "index"');
|
|
}
|
|
if (!options.tsConfig) {
|
|
throw new Error('Missing executor options "tsConfig"');
|
|
}
|
|
// cypress defaults aot to false so we cannot use buildOptimizer
|
|
// otherwise the 'buildOptimizer' cannot be used without 'aot' error is thrown
|
|
options.buildOptimizer = false;
|
|
options.aot = false;
|
|
options.assets ??= [];
|
|
options.allowedCommonJsDependencies ??= [];
|
|
options.budgets ??= [];
|
|
options.commonChunk ??= true;
|
|
options.crossOrigin ??= 'none';
|
|
options.deleteOutputPath ??= true;
|
|
options.extractLicenses ??= true;
|
|
options.fileReplacements ??= [];
|
|
options.inlineStyleLanguage ??= 'css';
|
|
options.i18nDuplicateTranslation ??= 'warning';
|
|
options.outputHashing ??= 'none';
|
|
options.progress ??= true;
|
|
options.scripts ??= [];
|
|
options.main ??= options.browser;
|
|
return options;
|
|
}
|
|
/**
|
|
* @returns a path from the workspace root to a temp file containing the base tailwind setup
|
|
* if tailwind is being used in the project root or workspace root
|
|
* this file should get cleaned up via the cypress executor
|
|
*/
|
|
function getTempStylesForTailwind(ctExecutorContext) {
|
|
const ctProjectConfig = ctExecutorContext.projectGraph.nodes[ctExecutorContext.projectName]?.data;
|
|
// angular only supports `tailwind.config.{js,cjs}`
|
|
const ctProjectTailwindConfig = (0, path_1.join)(ctExecutorContext.root, ctProjectConfig.root, 'tailwind.config');
|
|
const exts = ['js', 'cjs'];
|
|
const isTailWindInCtProject = exts.some((ext) => (0, fs_1.existsSync)(`${ctProjectTailwindConfig}.${ext}`));
|
|
const rootTailwindPath = (0, path_1.join)(ctExecutorContext.root, 'tailwind.config');
|
|
const isTailWindInRoot = exts.some((ext) => (0, fs_1.existsSync)(`${rootTailwindPath}.${ext}`));
|
|
if (isTailWindInRoot || isTailWindInCtProject) {
|
|
const pathToStyle = (0, ct_helpers_1.getTempTailwindPath)(ctExecutorContext);
|
|
try {
|
|
(0, fs_1.mkdirSync)((0, path_1.dirname)(pathToStyle), { recursive: true });
|
|
(0, fs_1.writeFileSync)(pathToStyle, `
|
|
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
`, { encoding: 'utf-8' });
|
|
return pathToStyle;
|
|
}
|
|
catch (makeTmpFileError) {
|
|
devkit_1.logger.warn((0, devkit_1.stripIndents) `Issue creating a temp file for tailwind styles. Defaulting to no tailwind setup.
|
|
Temp file path? ${pathToStyle}`);
|
|
devkit_1.logger.error(makeTmpFileError);
|
|
}
|
|
}
|
|
}
|
|
function isOffsetNeeded(ctExecutorContext, ctProjectConfig) {
|
|
try {
|
|
const supportsWorkspaceRoot = isCyVersionGreaterThanOrEqual('12.9.0');
|
|
// if using cypress <v12.9.0 then we require the offset
|
|
if (!supportsWorkspaceRoot) {
|
|
return true;
|
|
}
|
|
if (ctProjectConfig.projectType === 'library' &&
|
|
// angular will only see this config if the library root is the build project config root
|
|
// otherwise it will be set to the buildTarget root which is the app root where this config doesn't exist
|
|
// causing tailwind styles from the libs project root to not work
|
|
['js', 'cjs'].some((ext) => (0, fs_1.existsSync)((0, path_1.join)(ctExecutorContext.root, ctProjectConfig.root, `tailwind.config.${ext}`)))) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
catch (e) {
|
|
if (process.env.NX_VERBOSE_LOGGING === 'true') {
|
|
devkit_1.logger.error(e);
|
|
}
|
|
// unable to determine if we don't require an offset
|
|
// safest to assume we do
|
|
return true;
|
|
}
|
|
}
|
|
/**
|
|
* check if the cypress version is able to understand absolute paths to the indexHtmlFile option
|
|
* this is required for nx to work with cypress <v12.17.0 since the relative pathing is causes issues
|
|
* with invalid pathing.
|
|
* v12.17.0+ works with relative pathing
|
|
*
|
|
* if there is an error thrown then we assume it is an older version of cypress and use the absolute path
|
|
* as that was supported for longer.
|
|
*
|
|
* */
|
|
function requiresAbsolutePath() {
|
|
try {
|
|
return !isCyVersionGreaterThanOrEqual('12.17.0');
|
|
}
|
|
catch (e) {
|
|
if (process.env.NX_VERBOSE_LOGGING === 'true') {
|
|
devkit_1.logger.error(e);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
/**
|
|
* Checks if the install cypress version is greater than or equal to the provided version.
|
|
* Does not catch errors as any custom logic for error handling is required on consumer side.
|
|
* */
|
|
function isCyVersionGreaterThanOrEqual(version) {
|
|
const { version: cyVersion = null } = require('cypress/package.json');
|
|
return !!cyVersion && (0, semver_1.gte)(cyVersion, version);
|
|
}
|
|
|