Skip to content

Commit

Permalink
fix(testing): support custom workspaceRoot for angular CT (#15485)
Browse files Browse the repository at this point in the history
(cherry picked from commit 26fbd1d)
  • Loading branch information
barbados-clemens authored and FrozenPandaz committed Apr 24, 2023
1 parent 44a0c83 commit 9dfc66c
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 67 deletions.
34 changes: 34 additions & 0 deletions e2e/angular-extensions/src/cypress-component-tests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {
updateFile,
updateProjectConfig,
removeFile,
checkFilesExist,
} from '../../utils';
import { names } from '@nrwl/devkit';
import { join } from 'path';

describe('Angular Cypress Component Tests', () => {
let projectName: string;
Expand Down Expand Up @@ -114,6 +116,20 @@ describe('Angular Cypress Component Tests', () => {
);
}
});

it('should use root level tailwinds config', () => {
useRootLevelTailwindConfig(
join('libs', buildableLibName, 'tailwind.config.js')
);
checkFilesExist('tailwind.config.js');
checkFilesDoNotExist(`libs/${buildableLibName}/tailwind.config.js`);

if (runCypressTests()) {
expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain(
'All specs passed!'
);
}
});
});

function createApp(appName: string) {
Expand Down Expand Up @@ -386,3 +402,21 @@ function updateBuilableLibTestsToAssertAppStyles(
}
);
}

function useRootLevelTailwindConfig(existingConfigPath: string) {
createFile(
'tailwind.config.js',
`const { join } = require('path');
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [join(__dirname, '**/*.{html,js,ts}')],
theme: {
extend: {},
},
plugins: [],
};
`
);
removeFile(existingConfigPath);
}
201 changes: 135 additions & 66 deletions packages/angular/plugins/component-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ import {
readCachedProjectGraph,
readTargetOptions,
stripIndents,
workspaceRoot,
} from '@nrwl/devkit';
import { existsSync, lstatSync, mkdirSync, writeFileSync } from 'fs';
import { dirname, join, relative, sep } from 'path';
import type { BrowserBuilderSchema } from '../src/builders/webpack-browser/schema';
import { gte } from 'semver';

/**
* Angular nx preset for Cypress Component Testing
Expand Down Expand Up @@ -78,11 +78,14 @@ ${e.stack ? e.stack : e}`
Has project config? ${!!graph.nodes?.[buildTarget.project]?.data}`);
}

const fromWorkspaceRoot = relative(workspaceRoot, pathToConfig);
const fromWorkspaceRoot = relative(ctContext.root, pathToConfig);
const normalizedFromWorkspaceRootPath = lstatSync(pathToConfig).isFile()
? dirname(fromWorkspaceRoot)
: fromWorkspaceRoot;
const offset = offsetFromRoot(normalizedFromWorkspaceRootPath);
const offset = isOffsetNeeded(ctContext, ctProjectConfig)
? offsetFromRoot(normalizedFromWorkspaceRootPath)
: undefined;

const buildContext = createExecutorContext(
graph,
graph.nodes[buildTarget.project]?.data.targets,
Expand All @@ -101,6 +104,15 @@ ${e.stack ? e.stack : e}`
...nxBaseCypressPreset(pathToConfig),
// NOTE: cannot use a glob pattern since it will break cypress generated tsconfig.
specPattern: ['src/**/*.cy.ts', 'src/**/*.cy.js'],
// cypress defaults to a relative path from the workspaceRoot instead of projectRoot
// set as absolute path in case this changes internally to cypress, this path isn't OS dependent
indexHtmlFile: joinPathFragments(
ctContext.root,
ctProjectConfig.root,
'cypress',
'support',
'component-index.html'
),
devServer: {
// cypress uses string union type,
// need to use const to prevent typing to string
Expand Down Expand Up @@ -156,8 +168,12 @@ function getBuildableTarget(ctContext: ExecutorContext) {
function normalizeBuildTargetOptions(
buildContext: ExecutorContext,
ctContext: ExecutorContext,
offset: string
): { root: string; sourceRoot: string; buildOptions: BrowserBuilderSchema } {
offset?: string
): {
root: string;
sourceRoot: string;
buildOptions: BrowserBuilderSchema & { workspaceRoot: string };
} {
const options = readTargetOptions<BrowserBuilderSchema>(
{
project: buildContext.projectName,
Expand All @@ -168,39 +184,40 @@ function normalizeBuildTargetOptions(
);
const buildOptions = withSchemaDefaults(options);

// 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: string) => {
const maybeFullPath = join(workspaceRoot, polyfill.split('/').join(sep));
if (existsSync(maybeFullPath)) {
return joinPathFragments(offset, polyfill);
}
return polyfill;
};
// paths need to be unix paths for angular devkit
buildOptions.polyfills =
Array.isArray(buildOptions.polyfills) && buildOptions.polyfills.length > 0
? (buildOptions.polyfills as string[]).map((p) => handlePolyfillPath(p))
: handlePolyfillPath(buildOptions.polyfills as string);

buildOptions.main = joinPathFragments(offset, buildOptions.main);
buildOptions.index =
typeof buildOptions.index === 'string'
? joinPathFragments(offset, buildOptions.index)
: {
...buildOptions.index,
input: joinPathFragments(offset, buildOptions.index.input),
};
// cypress creates a tsconfig if one isn't preset
// that contains all the support required for angular and component tests
delete buildOptions.tsConfig;

buildOptions.fileReplacements = buildOptions.fileReplacements.map((fr) => {
fr.replace = joinPathFragments(offset, fr.replace);
fr.with = joinPathFragments(offset, fr.with);
return fr;
});
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: string) => {
const maybeFullPath = join(ctContext.root, polyfill.split('/').join(sep));
if (existsSync(maybeFullPath)) {
return joinPathFragments(offset, polyfill);
}
return polyfill;
};
// paths need to be unix paths for angular devkit
buildOptions.polyfills =
Array.isArray(buildOptions.polyfills) && buildOptions.polyfills.length > 0
? (buildOptions.polyfills as string[]).map((p) => handlePolyfillPath(p))
: handlePolyfillPath(buildOptions.polyfills as string);
buildOptions.main = joinPathFragments(offset, buildOptions.main);
buildOptions.index =
typeof buildOptions.index === 'string'
? joinPathFragments(offset, buildOptions.index)
: {
...buildOptions.index,
input: joinPathFragments(offset, buildOptions.index.input),
};
buildOptions.fileReplacements = buildOptions.fileReplacements.map((fr) => {
fr.replace = joinPathFragments(offset, fr.replace);
fr.with = 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
Expand All @@ -213,29 +230,31 @@ function normalizeBuildTargetOptions(
ctContext.projectName
)
) {
buildOptions.assets = buildOptions.assets.map((asset) => {
return typeof asset === 'string'
? joinPathFragments(offset, asset)
: { ...asset, input: joinPathFragments(offset, asset.input) };
});
buildOptions.styles = buildOptions.styles.map((style) => {
return typeof style === 'string'
? joinPathFragments(offset, style)
: { ...style, input: joinPathFragments(offset, style.input) };
});
buildOptions.scripts = buildOptions.scripts.map((script) => {
return typeof script === 'string'
? joinPathFragments(offset, script)
: { ...script, input: joinPathFragments(offset, script.input) };
});
if (buildOptions.stylePreprocessorOptions?.includePaths.length > 0) {
buildOptions.stylePreprocessorOptions = {
includePaths: buildOptions.stylePreprocessorOptions.includePaths.map(
(path) => {
return joinPathFragments(offset, path);
}
),
};
if (offset) {
buildOptions.assets = buildOptions.assets.map((asset) => {
return typeof asset === 'string'
? joinPathFragments(offset, asset)
: { ...asset, input: joinPathFragments(offset, asset.input) };
});
buildOptions.styles = buildOptions.styles.map((style) => {
return typeof style === 'string'
? joinPathFragments(offset, style)
: { ...style, input: joinPathFragments(offset, style.input) };
});
buildOptions.scripts = buildOptions.scripts.map((script) => {
return typeof script === 'string'
? joinPathFragments(offset, script)
: { ...script, input: joinPathFragments(offset, script.input) };
});
if (buildOptions.stylePreprocessorOptions?.includePaths.length > 0) {
buildOptions.stylePreprocessorOptions = {
includePaths: buildOptions.stylePreprocessorOptions.includePaths.map(
(path) => {
return joinPathFragments(offset, path);
}
),
};
}
}
} else {
const stylePath = getTempStylesForTailwind(ctContext);
Expand All @@ -256,9 +275,15 @@ Note: this may fail, setting the correct 'sourceRoot' for ${buildContext.project
}

return {
root: joinPathFragments(offset, config.root),
sourceRoot: joinPathFragments(offset, config.sourceRoot),
buildOptions,
root: offset ? joinPathFragments(offset, config.root) : config.root,
sourceRoot: offset
? joinPathFragments(offset, config.sourceRoot)
: config.sourceRoot,
buildOptions: {
...buildOptions,
// this property is only valid for cy v12.9.0+
workspaceRoot: offset ? undefined : ctContext.root,
},
};
}

Expand Down Expand Up @@ -309,13 +334,14 @@ function getTempStylesForTailwind(ctExecutorContext: ExecutorContext) {
ctProjectConfig.root,
'tailwind.config'
);
const isTailWindInCtProject =
existsSync(ctProjectTailwindConfig + '.js') ||
existsSync(ctProjectTailwindConfig + '.cjs');
const exts = ['js', 'cjs'];
const isTailWindInCtProject = exts.some((ext) =>
existsSync(`${ctProjectTailwindConfig}.${ext}`)
);
const rootTailwindPath = join(ctExecutorContext.root, 'tailwind.config');
const isTailWindInRoot =
existsSync(rootTailwindPath + '.js') ||
existsSync(rootTailwindPath + '.cjs');
const isTailWindInRoot = exts.some((ext) =>
existsSync(`${rootTailwindPath}.${ext}`)
);

if (isTailWindInRoot || isTailWindInCtProject) {
const pathToStyle = getTempTailwindPath(ctExecutorContext);
Expand All @@ -339,3 +365,46 @@ function getTempStylesForTailwind(ctExecutorContext: ExecutorContext) {
}
}
}

function isOffsetNeeded(
ctExecutorContext: ExecutorContext,
ctProjectConfig: ProjectConfiguration
) {
try {
const { version = null } = require('cypress/package.json');

const supportsWorkspaceRoot = !!version && gte(version, '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) =>
existsSync(
join(
ctExecutorContext.root,
ctProjectConfig.root,
`tailwind.config.${ext}`
)
)
)
) {
return true;
}

return false;
} catch (e) {
if (process.env.NX_VERBOSE_LOGGING === 'true') {
logger.error(e);
}
// unable to determine if we don't require an offset
// safest to assume we do
return true;
}
}
2 changes: 1 addition & 1 deletion packages/cypress/src/utils/ct-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function getTempTailwindPath(context: ExecutorContext) {
}

/**
* Checks if the childProjectName is a decendent of the parentProjectName
* Checks if the childProjectName is a descendent of the parentProjectName
* in the project graph
**/
export function isCtProjectUsingBuildProject(
Expand Down
2 changes: 2 additions & 0 deletions scripts/depcheck/missing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ const IGNORE_MATCHES_IN_PACKAGE = {
'sass',
'stylus',
'tailwindcss',
// used in the CT angular plugin where Cy is already installed to use it.
'cypress',
],
cli: ['nx'],
cypress: ['cypress', '@angular-devkit/schematics', '@nrwl/cypress', 'vite'],
Expand Down

0 comments on commit 9dfc66c

Please sign in to comment.