Skip to content

Commit

Permalink
support namespaced definitions (#25804)
Browse files Browse the repository at this point in the history
  • Loading branch information
lmiller1990 authored Feb 15, 2023
1 parent f61995b commit bfa2d60
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 51 deletions.
3 changes: 2 additions & 1 deletion cli/types/cypress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3298,7 +3298,8 @@ declare namespace Cypress {

interface ResolvedComponentFrameworkDefinition {
/**
* A semantic, unique identifier. Must begin with `cypress-ct-` for third party implementations.
* A semantic, unique identifier.
* Must begin with `cypress-ct-` or `@org/cypress-ct-` for third party implementations.
* @example 'reactscripts'
* @example 'nextjs'
* @example 'cypress-ct-solid-js'
Expand Down
13 changes: 12 additions & 1 deletion npm/webpack-dev-server/src/devServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,22 @@ export type PresetHandlerResult = { frameworkConfig: Configuration, sourceWebpac

type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>

const thirdPartyDefinitionPrefixes = {
// matches @org/cypress-ct-*
namespacedPrefixRe: /^@.+?\/cypress-ct-.+/,
globalPrefix: 'cypress-ct-',
}

export function isThirdPartyDefinition (framework: string) {
return framework.startsWith(thirdPartyDefinitionPrefixes.globalPrefix) ||
thirdPartyDefinitionPrefixes.namespacedPrefixRe.test(framework)
}

async function getPreset (devServerConfig: WebpackDevServerConfig): Promise<Optional<PresetHandlerResult, 'frameworkConfig'>> {
const defaultWebpackModules = () => ({ sourceWebpackModulesResult: sourceDefaultWebpackDependencies(devServerConfig) })

// Third party library (eg solid-js, lit, etc)
if (devServerConfig.framework?.startsWith('cypress-ct-')) {
if (devServerConfig.framework && isThirdPartyDefinition(devServerConfig.framework)) {
return defaultWebpackModules()
}

Expand Down
68 changes: 27 additions & 41 deletions packages/scaffold-config/src/ct-detect-third-party.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from 'path'
import globby from 'globby'
import { z } from 'zod'
import fs from 'fs-extra'
import Debug from 'debug'

const debug = Debug('cypress:scaffold-config:ct-detect-third-party')
Expand All @@ -18,54 +19,41 @@ const DependencyArraySchema = z.array(DependencySchema)

const BundlerSchema = z.enum(['webpack', 'vite'])

const thirdPartyDefinitionPrefixes = {
// matches @org/cypress-ct-*
namespacedPrefixRe: /^@.+?\/cypress-ct-.+/,
globalPrefix: 'cypress-ct-',
}

export function isThirdPartyDefinition (definition: Cypress.ComponentFrameworkDefinition | Cypress.ThirdPartyComponentFrameworkDefinition): definition is Cypress.ThirdPartyComponentFrameworkDefinition {
return definition.type.startsWith(thirdPartyDefinitionPrefixes.globalPrefix) ||
thirdPartyDefinitionPrefixes.namespacedPrefixRe.test(definition.type)
}

const ThirdPartyComponentFrameworkSchema = z.object({
type: z.string().startsWith('cypress-ct-'),
type: z.string().startsWith(thirdPartyDefinitionPrefixes.globalPrefix).or(z.string().regex(thirdPartyDefinitionPrefixes.namespacedPrefixRe)),
name: z.string(),
supportedBundlers: z.array(BundlerSchema),
detectors: DependencyArraySchema,
dependencies: z.function(),
componentIndexHtml: z.optional(z.function()),
})

const CT_FRAMEWORK_GLOB = path.join('node_modules', 'cypress-ct-*', 'package.json')

// tsc will compile `import(...)` calls to require unless a different tsconfig.module value
// is used (e.g. module=node16). To change this, we would also have to change the ts-node behavior when requiring the
// Cypress config file. This hack for keeping dynamic imports from being converted works across all
// of our supported node versions

// const _dynamicImport = new Function('specifier', 'return import(specifier)')

// const dynamicImport = <T>(module: string) => {
// return _dynamicImport(module) as Promise<T>
// }

// const dynamicAbsoluteImport = (filePath: string) => {
// return dynamicImport(pathToFileURL(filePath).href) as Promise<any>
// }

/**
* When compiling CJS -> ESM, TS can produce:
* Imported [Module: null prototype] { __esModule: true, default: { default: { type: 'cypress-ct-solid-js', ... } } }
* We just keep getting `default` property until none exists.
*/
function getDefaultExport<T extends { default?: T }> (mod: T): T {
if (mod?.default) {
return getDefaultExport(mod.default)
}

return mod?.default ?? mod
}
const CT_FRAMEWORK_GLOBAL_GLOB = path.join('node_modules', 'cypress-ct-*', 'package.json')
const CT_FRAMEWORK_NAMESPACED_GLOB = path.join('node_modules', '@*?/cypress-ct-*?', 'package.json')

export async function detectThirdPartyCTFrameworks (
projectRoot: string,
): Promise<Cypress.ThirdPartyComponentFrameworkDefinition[]> {
try {
const fullPathGlob = path.join(projectRoot, CT_FRAMEWORK_GLOB).replaceAll('\\', '/')
const fullPathGlobs = [
path.join(projectRoot, CT_FRAMEWORK_GLOBAL_GLOB),
path.join(projectRoot, CT_FRAMEWORK_NAMESPACED_GLOB),
].map((x) => x.replaceAll('\\', '/'))

const packageJsonPaths = await globby(fullPathGlob)
const packageJsonPaths = await globby(fullPathGlobs)

debug('Found packages matching %s glob: %o', fullPathGlob, packageJsonPaths)
debug('Found packages matching %s glob: %o', fullPathGlobs, packageJsonPaths)

const modules = await Promise.all(
packageJsonPaths.map(async (packageJsonPath) => {
Expand All @@ -86,21 +74,19 @@ export async function detectThirdPartyCTFrameworks (
* }
* }
*/
const packageName = path.basename(path.dirname(packageJsonPath))
const pkgJson = await fs.readJSON(packageJsonPath)

debug('Attempting to resolve third party module with require.resolve: %s', packageName)
debug('`name` in package.json', pkgJson.name)

const modulePath = require.resolve(packageName, { paths: [projectRoot] })
debug('Attempting to resolve third party module with require.resolve: %s', pkgJson.name)

const modulePath = require.resolve(pkgJson.name, { paths: [projectRoot] })

debug('Resolve successful: %s', modulePath)

debug('require(%s)', modulePath)

const m = require(modulePath)

debug('Imported %o', m)

const mod = getDefaultExport(m)
const mod = require(modulePath)

debug('Module is %o', mod)

Expand Down
6 changes: 2 additions & 4 deletions packages/scaffold-config/src/frameworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as dependencies from './dependencies'
import componentIndexHtmlGenerator from './component-index-template'
import debugLib from 'debug'
import semver from 'semver'
import { isThirdPartyDefinition } from './ct-detect-third-party'

const debug = debugLib('cypress:scaffold-config:frameworks')

Expand Down Expand Up @@ -281,16 +282,13 @@ export const CT_FRAMEWORKS: Cypress.ComponentFrameworkDefinition[] = [
},
]

function isThirdPartyDefinition (definition: Cypress.ComponentFrameworkDefinition | Cypress.ThirdPartyComponentFrameworkDefinition): definition is Cypress.ThirdPartyComponentFrameworkDefinition {
return definition.type.startsWith('cypress-ct')
}
/**
* Given a first or third party Component Framework Definition,
* resolves into a unified ResolvedComponentFrameworkDefinition.
* This way we have a single type used throughout Cypress.
*/
export function resolveComponentFrameworkDefinition (definition: Cypress.ComponentFrameworkDefinition | Cypress.ThirdPartyComponentFrameworkDefinition): Cypress.ResolvedComponentFrameworkDefinition {
const thirdParty = isThirdPartyDefinition(definition) // type.startsWith('cypress-ct-')
const thirdParty = isThirdPartyDefinition(definition)

const dependencies: Cypress.ResolvedComponentFrameworkDefinition['dependencies'] = async (bundler, projectPath) => {
const declaredDeps = definition.dependencies(bundler)
Expand Down
46 changes: 42 additions & 4 deletions packages/scaffold-config/test/unit/ct-detect-third-party.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { scaffoldMigrationProject, fakeDepsInNodeModules } from './detect.spec'
import fs from 'fs-extra'
import path from 'path'
import { detectThirdPartyCTFrameworks, validateThirdPartyModule } from '../../src'
import { detectThirdPartyCTFrameworks, validateThirdPartyModule, isThirdPartyDefinition } from '../../src'
import { expect } from 'chai'
import solidJs from './fixtures'

async function scaffoldQwikApp (thirdPartyModuleNames: Array<'cypress-ct-qwik' | 'misconfigured-cypress-ct-qwik'>) {
async function scaffoldQwikApp (thirdPartyModuleNames: Array<'cypress-ct-qwik' | '@org/cypress-ct-qwik' | 'misconfigured-cypress-ct-qwik'>) {
const projectRoot = await scaffoldMigrationProject('qwik-app')

await fakeDepsInNodeModules(projectRoot, [{ dependency: '@builder.io/qwik', version: '0.17.5' }])
fakeDepsInNodeModules(projectRoot, [{ dependency: '@builder.io/qwik', version: '0.17.5' }])
for (const thirdPartyModuleName of thirdPartyModuleNames) {
const nodeModulePath = path.join(projectRoot, 'node_modules', thirdPartyModuleName)

Expand All @@ -19,15 +19,53 @@ async function scaffoldQwikApp (thirdPartyModuleNames: Array<'cypress-ct-qwik' |
return projectRoot
}

describe('isThirdPartyDefinition', () => {
context('global package', () => {
it('returns false for invalid prefix', () => {
const res = isThirdPartyDefinition({ ...solidJs, type: 'non-cypress-ct' })

expect(res).to.be.false
})

it('returns true for valid prefix', () => {
const res = isThirdPartyDefinition({ ...solidJs, type: 'cypress-ct-solid-js' })

expect(res).to.be.true
})
})

context('namespaced package', () => {
it('returns false for non third party with namespace', () => {
const res = isThirdPartyDefinition({ ...solidJs, type: '@org/non-cypress-ct' })

expect(res).to.be.false
})

it('returns true for third party with namespace', () => {
const res = isThirdPartyDefinition({ ...solidJs, type: '@org/cypress-ct-solid-js' })

expect(res).to.be.true
})
})
})

describe('detectThirdPartyCTFrameworks', () => {
it('detects third party frameworks', async () => {
it('detects third party frameworks in global namespace', async () => {
const projectRoot = await scaffoldQwikApp(['cypress-ct-qwik'])

const thirdPartyFrameworks = await detectThirdPartyCTFrameworks(projectRoot)

expect(thirdPartyFrameworks[0].type).eq('cypress-ct-qwik')
})

it('detects third party frameworks in org namespace', async () => {
const projectRoot = await scaffoldQwikApp(['@org/cypress-ct-qwik'])

const thirdPartyFrameworks = await detectThirdPartyCTFrameworks(projectRoot)

expect(thirdPartyFrameworks[0].type).eq('@org/cypress-ct-qwik')
})

it('ignores misconfigured third party frameworks', async () => {
const projectRoot = await scaffoldQwikApp(['cypress-ct-qwik', 'misconfigured-cypress-ct-qwik'])

Expand Down
28 changes: 28 additions & 0 deletions system-tests/projects/qwik-app/@org/cypress-ct-qwik/definition.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const qwikDep = {
type: 'qwik',
name: 'Qwik',
package: '@builder.io/qwik',
installer: '@builder.io/qwik',
description:
'An Open-Source sub-framework designed with a focus on server-side-rendering, lazy-loading, and styling/animation.',
minVersion: '^0.17.5',
}

module.exports = {
type: '@org/cypress-ct-qwik',

category: 'library',

name: 'Qwik',

supportedBundlers: ['vite'],

detectors: [qwikDep],

// Cypress will include the bundler dependency here, if they selected one.
dependencies: () => {
return [qwikDep]
},

icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 274"><defs><linearGradient id="logosQwik0" x1="22.347%" x2="77.517%" y1="49.545%" y2="50.388%"><stop offset="0%" stop-color="#4340C4"/><stop offset="12%" stop-color="#4642C8"/><stop offset="100%" stop-color="#594EE4"/></linearGradient><linearGradient id="logosQwik1" x1="38.874%" x2="60.879%" y1="49.845%" y2="50.385%"><stop offset="0%" stop-color="#4340C4"/><stop offset="74%" stop-color="#534ADB"/><stop offset="100%" stop-color="#594EE4"/></linearGradient><linearGradient id="logosQwik2" x1="-.004%" x2="100.123%" y1="49.529%" y2="50.223%"><stop offset="0%" stop-color="#4340C4"/><stop offset="23%" stop-color="#4340C4"/><stop offset="60%" stop-color="#4F48D5"/><stop offset="100%" stop-color="#594EE4"/></linearGradient><linearGradient id="logosQwik3" x1="35.4%" x2="64.895%" y1="49.459%" y2="50.085%"><stop offset="0%" stop-color="#0080FF"/><stop offset="100%" stop-color="#00B9FF"/></linearGradient><linearGradient id="logosQwik4" x1="-.243%" x2="100.411%" y1="49.366%" y2="50.467%"><stop offset="0%" stop-color="#0080FF"/><stop offset="17%" stop-color="#008BFF"/><stop offset="47%" stop-color="#00A7FF"/><stop offset="63%" stop-color="#00B9FF"/><stop offset="100%" stop-color="#00B9FF"/></linearGradient><linearGradient id="logosQwik5" x1="-.125%" x2="100.225%" y1="49.627%" y2="50.101%"><stop offset="0%" stop-color="#00B9FF"/><stop offset="30%" stop-color="#0080FF"/><stop offset="60%" stop-color="#2D67F1"/><stop offset="86%" stop-color="#4D55E8"/><stop offset="100%" stop-color="#594EE4"/></linearGradient><linearGradient id="logosQwik6" x1="4.557%" x2="99.354%" y1="50.184%" y2="51.298%"><stop offset="0%" stop-color="#4340C4"/><stop offset="12%" stop-color="#4642C8"/><stop offset="100%" stop-color="#594EE4"/></linearGradient></defs><path fill="url(#logosQwik0)" d="m175.051 236.859l25.162-15.071l49.298-86.929l-76.287 89.097z"/><path fill="url(#logosQwik1)" d="m242.337 80.408l-4.926-9.4l-1.932-3.663l-.2.196l-25.818-47.015C202.984 8.65 190.631 1.231 177.01 1.074l-25.074.206L188.15 114.8l-23.958 23.331l8.924 86.245l73.769-84.021c10.005-11.587 11.97-28.09 4.92-41.646l-9.466-18.302h-.002Z"/><path fill="url(#logosQwik2)" d="m201.113 72.256l-43.18-70.907L83.41.003C70.165-.15 57.83 6.573 50.88 17.87L7.01 101.747l34.443-33.334L84.701 8.356l97.894 112.153l18.3-18.626c8.397-8.142 5.54-19.558.22-29.625l-.002-.002Z"/><path fill="url(#logosQwik3)" d="M97.784 95.26L84.522 8.796l-73.148 88.03c-12.328 11.935-14.897 30.662-6.419 45.49l42.98 74.727c6.553 11.464 18.755 18.577 32.024 18.729l42.945.49L71.46 119.607L97.784 95.26Z"/><path fill="url(#logosQwik4)" d="M173.227 223.9L71.38 119.022l-13.196 12.59c-10.812 10.248-11.106 27.332-.728 37.921l43.99 66.384l70.65.907l1.127-12.926h.003Z"/><path fill="url(#logosQwik5)" d="m101.584 235.903l72.292-11.599l47.704 49.464z"/><path fill="url(#logosQwik6)" d="m173.111 224.483l27.168-3.457l24.096 49.915c1.06 2.06-1.719 3.977-3.373 2.302l-47.89-48.76Z"/><path fill="#FFF" d="M182.708 120.058L84.681 8.601l12.502 85.958L71.16 118.78l101.772 105.372l-7.595-85.905l17.371-18.192z"/></svg>',
}
3 changes: 3 additions & 0 deletions system-tests/projects/qwik-app/@org/cypress-ct-qwik/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function mount () {
return 'Legit mount function'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@org/cypress-ct-qwik",
"version": "1.0.0",
"main": "index.js",
"exports": {
"node": "./definition.cjs",
"default": "./index.mjs"
}
}

0 comments on commit bfa2d60

Please sign in to comment.