|
| 1 | +import * as fs from 'fs-extra' |
| 2 | +import { tmpdir } from 'os' |
| 3 | +import * as path from 'path' |
| 4 | +import { pathToFileURL } from 'url' |
| 5 | +import type { PresetHandlerResult, WebpackDevServerConfig } from '../devServer' |
| 6 | +import { sourceDefaultWebpackDependencies } from './sourceRelativeWebpackModules' |
| 7 | + |
| 8 | +export type AngularJsonProjectConfig = { |
| 9 | + projectType: string |
| 10 | + root: string |
| 11 | + sourceRoot: string |
| 12 | + architect: { |
| 13 | + build: { |
| 14 | + options: { [key: string]: any } & { polyfills?: string } |
| 15 | + configurations?: { |
| 16 | + [configuration: string]: { |
| 17 | + [key: string]: any |
| 18 | + } |
| 19 | + } |
| 20 | + } |
| 21 | + } |
| 22 | +} |
| 23 | + |
| 24 | +type AngularJson = { |
| 25 | + defaultProject?: string |
| 26 | + projects: { |
| 27 | + [project: string]: AngularJsonProjectConfig |
| 28 | + } |
| 29 | +} |
| 30 | + |
| 31 | +const dynamicImport = new Function('specifier', 'return import(specifier)') |
| 32 | + |
| 33 | +export async function angularHandler (devServerConfig: WebpackDevServerConfig): Promise<PresetHandlerResult> { |
| 34 | + const webpackConfig = await getAngularCliWebpackConfig(devServerConfig) |
| 35 | + |
| 36 | + return { frameworkConfig: webpackConfig, sourceWebpackModulesResult: sourceDefaultWebpackDependencies(devServerConfig) } |
| 37 | +} |
| 38 | + |
| 39 | +async function getAngularCliWebpackConfig (devServerConfig: WebpackDevServerConfig) { |
| 40 | + const { projectRoot } = devServerConfig.cypressConfig |
| 41 | + |
| 42 | + const { |
| 43 | + generateBrowserWebpackConfigFromContext, |
| 44 | + getCommonConfig, |
| 45 | + getStylesConfig, |
| 46 | + } = await getAngularCliModules(projectRoot) |
| 47 | + |
| 48 | + const angularJson = await getAngularJson(projectRoot) |
| 49 | + |
| 50 | + let { defaultProject } = angularJson |
| 51 | + |
| 52 | + if (!defaultProject) { |
| 53 | + defaultProject = Object.keys(angularJson.projects).find((name) => angularJson.projects[name].projectType === 'application') |
| 54 | + |
| 55 | + if (!defaultProject) { |
| 56 | + throw new Error('Could not find a project with projectType "application" in "angular.json"') |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + const defaultProjectConfig = angularJson.projects[defaultProject] |
| 61 | + |
| 62 | + const tsConfig = await generateTsConfig(devServerConfig, defaultProjectConfig) |
| 63 | + |
| 64 | + const buildOptions = getAngularBuildOptions(defaultProjectConfig, tsConfig) |
| 65 | + |
| 66 | + const context = createFakeContext(projectRoot, defaultProject, defaultProjectConfig) |
| 67 | + |
| 68 | + const { config } = await generateBrowserWebpackConfigFromContext( |
| 69 | + buildOptions, |
| 70 | + context, |
| 71 | + (wco: any) => [getCommonConfig(wco), getStylesConfig(wco)], |
| 72 | + ) |
| 73 | + |
| 74 | + delete config.entry.main |
| 75 | + |
| 76 | + return config |
| 77 | +} |
| 78 | + |
| 79 | +export function getAngularBuildOptions (projectConfig: AngularJsonProjectConfig, tsConfig: string) { |
| 80 | + // Default options are derived from the @angular-devkit/build-angular browser builder, with some options from |
| 81 | + // the serve builder thrown in for development. |
| 82 | + // see: https://github.com/angular/angular-cli/blob/main/packages/angular_devkit/build_angular/src/builders/browser/schema.json |
| 83 | + return { |
| 84 | + outputPath: 'dist/angular-app', |
| 85 | + assets: [], |
| 86 | + styles: [], |
| 87 | + scripts: [], |
| 88 | + budgets: [], |
| 89 | + fileReplacements: [], |
| 90 | + outputHashing: 'all', |
| 91 | + inlineStyleLanguage: 'css', |
| 92 | + stylePreprocessorOptions: { includePaths: [] }, |
| 93 | + resourcesOutputPath: undefined, |
| 94 | + commonChunk: true, |
| 95 | + baseHref: undefined, |
| 96 | + deployUrl: undefined, |
| 97 | + verbose: false, |
| 98 | + progress: false, |
| 99 | + i18nMissingTranslation: 'warning', |
| 100 | + i18nDuplicateTranslation: 'warning', |
| 101 | + localize: undefined, |
| 102 | + watch: true, |
| 103 | + poll: undefined, |
| 104 | + deleteOutputPath: true, |
| 105 | + preserveSymlinks: undefined, |
| 106 | + showCircularDependencies: false, |
| 107 | + subresourceIntegrity: false, |
| 108 | + serviceWorker: false, |
| 109 | + ngswConfigPath: undefined, |
| 110 | + statsJson: false, |
| 111 | + webWorkerTsConfig: undefined, |
| 112 | + crossOrigin: 'none', |
| 113 | + allowedCommonJsDependencies: [], // Add Cypress 'browser.js' entry point to ignore "CommonJS or AMD dependencies can cause optimization bailouts." warning |
| 114 | + buildOptimizer: false, |
| 115 | + optimization: false, |
| 116 | + vendorChunk: true, |
| 117 | + extractLicenses: false, |
| 118 | + sourceMap: true, |
| 119 | + namedChunks: true, |
| 120 | + ...projectConfig.architect.build.options, |
| 121 | + ...projectConfig.architect.build.configurations?.development || {}, |
| 122 | + tsConfig, |
| 123 | + aot: false, |
| 124 | + } |
| 125 | +} |
| 126 | + |
| 127 | +export async function generateTsConfig (devServerConfig: WebpackDevServerConfig, projectConfig: AngularJsonProjectConfig): Promise<string> { |
| 128 | + const { cypressConfig } = devServerConfig |
| 129 | + const { projectRoot } = cypressConfig |
| 130 | + |
| 131 | + const specPattern = Array.isArray(cypressConfig.specPattern) ? cypressConfig.specPattern : [cypressConfig.specPattern] |
| 132 | + |
| 133 | + const getProjectFilePath = (...fileParts: string[]): string => toPosix(path.join(projectRoot, ...fileParts)) |
| 134 | + |
| 135 | + const includePaths = [...specPattern.map((pattern) => getProjectFilePath(pattern))] |
| 136 | + |
| 137 | + if (cypressConfig.supportFile) { |
| 138 | + includePaths.push(toPosix(cypressConfig.supportFile)) |
| 139 | + } |
| 140 | + |
| 141 | + if (projectConfig.architect.build.options.polyfills) { |
| 142 | + const polyfills = getProjectFilePath(projectConfig.architect.build.options.polyfills) |
| 143 | + |
| 144 | + includePaths.push(polyfills) |
| 145 | + } |
| 146 | + |
| 147 | + const cypressTypes = getProjectFilePath('node_modules', 'cypress', 'types', 'index.d.ts') |
| 148 | + |
| 149 | + includePaths.push(cypressTypes) |
| 150 | + |
| 151 | + const tsConfigContent = JSON.stringify({ |
| 152 | + extends: getProjectFilePath('tsconfig.json'), |
| 153 | + compilerOptions: { |
| 154 | + outDir: getProjectFilePath('out-tsc/cy'), |
| 155 | + allowSyntheticDefaultImports: true, |
| 156 | + }, |
| 157 | + include: includePaths, |
| 158 | + }) |
| 159 | + |
| 160 | + const tsConfigPath = path.join(await getTempDir(), 'tsconfig.json') |
| 161 | + |
| 162 | + await fs.writeFile(tsConfigPath, tsConfigContent) |
| 163 | + |
| 164 | + return tsConfigPath |
| 165 | +} |
| 166 | + |
| 167 | +export async function getTempDir (): Promise<string> { |
| 168 | + const cypressTempDir = path.join(tmpdir(), 'cypress-angular-ct') |
| 169 | + |
| 170 | + await fs.ensureDir(cypressTempDir) |
| 171 | + |
| 172 | + return cypressTempDir |
| 173 | +} |
| 174 | + |
| 175 | +export async function getAngularCliModules (projectRoot: string) { |
| 176 | + const [ |
| 177 | + { generateBrowserWebpackConfigFromContext }, |
| 178 | + { getCommonConfig }, |
| 179 | + { getStylesConfig }, |
| 180 | + ] = await Promise.all([ |
| 181 | + '@angular-devkit/build-angular/src/utils/webpack-browser-config.js', |
| 182 | + '@angular-devkit/build-angular/src/webpack/configs/common.js', |
| 183 | + '@angular-devkit/build-angular/src/webpack/configs/styles.js', |
| 184 | + ].map((dep) => { |
| 185 | + try { |
| 186 | + const depPath = require.resolve(dep, { paths: [projectRoot] }) |
| 187 | + |
| 188 | + const url = pathToFileURL(depPath).href |
| 189 | + |
| 190 | + return dynamicImport(url) |
| 191 | + } catch (e) { |
| 192 | + throw new Error(`Could not resolve "${dep}". Do you have "@angular-devkit/build-angular" installed?`) |
| 193 | + } |
| 194 | + })) |
| 195 | + |
| 196 | + return { |
| 197 | + generateBrowserWebpackConfigFromContext, |
| 198 | + getCommonConfig, |
| 199 | + getStylesConfig, |
| 200 | + } |
| 201 | +} |
| 202 | + |
| 203 | +export async function getAngularJson (projectRoot: string): Promise<AngularJson> { |
| 204 | + const { findUp } = await dynamicImport('find-up') as typeof import('find-up') |
| 205 | + |
| 206 | + const angularJsonPath = await findUp('angular.json', { cwd: projectRoot }) |
| 207 | + |
| 208 | + if (!angularJsonPath) { |
| 209 | + throw new Error(`Could not find angular.json. Looked in ${projectRoot} and up.`) |
| 210 | + } |
| 211 | + |
| 212 | + const angularJson = await fs.readFile(angularJsonPath, 'utf8') |
| 213 | + |
| 214 | + return JSON.parse(angularJson) |
| 215 | +} |
| 216 | + |
| 217 | +function createFakeContext (projectRoot: string, defaultProject: string, defaultProjectConfig: any) { |
| 218 | + const logger = { |
| 219 | + createChild: () => ({}), |
| 220 | + } |
| 221 | + |
| 222 | + const context = { |
| 223 | + target: { |
| 224 | + project: defaultProject, |
| 225 | + }, |
| 226 | + workspaceRoot: projectRoot, |
| 227 | + getProjectMetadata: () => { |
| 228 | + return { |
| 229 | + root: defaultProjectConfig.root, |
| 230 | + sourceRoot: defaultProjectConfig.root, |
| 231 | + projectType: 'application', |
| 232 | + } |
| 233 | + }, |
| 234 | + logger, |
| 235 | + } |
| 236 | + |
| 237 | + return context |
| 238 | +} |
| 239 | + |
| 240 | +export const toPosix = (filePath: string) => filePath.split(path.sep).join(path.posix.sep) |
0 commit comments