diff --git a/e2e/qwik-nx-e2e/tests/application-basic-behavior.suite.ts b/e2e/qwik-nx-e2e/tests/application-basic-behavior.suite.ts index a8aefab4..5d3b4ebd 100644 --- a/e2e/qwik-nx-e2e/tests/application-basic-behavior.suite.ts +++ b/e2e/qwik-nx-e2e/tests/application-basic-behavior.suite.ts @@ -9,6 +9,7 @@ import { runCommandUntil, promisifiedTreeKill, killPort, + stripConsoleColors, } from '@qwikifiers/e2e/utils'; export function testApplicationBasicBehavior(generator: 'app' | 'preset') { @@ -35,9 +36,18 @@ export function testApplicationBasicBehavior(generator: 'app' | 'preset') { }); it('should create qwik-nx', async () => { - const result = await runNxCommandAsync(`build-ssr ${project}`); + const result = await runNxCommandAsync(`build ${project}`); + expect(stripConsoleColors(result.stdout.replace(/\n|\s/g, ''))).toContain( + [ + 'Targets to be executed:', + `${project}:build.client`, + `${project}:build.ssr`, + ] + .join('') + .replace(/\n|\s/g, '') + ); expect(result.stdout).toContain( - `Successfully ran target build-ssr for project ${project}` + `Successfully ran target build for project ${project}` ); expect(() => checkFilesExist(`dist/apps/${project}/client/q-manifest.json`) @@ -47,6 +57,25 @@ export function testApplicationBasicBehavior(generator: 'app' | 'preset') { ).not.toThrow(); }, 200000); + it('should run build with a specified configuration', async () => { + // TODO: cloudflare pages or custom configurations should also be tested + const result = await runNxCommandAsync( + `build ${project} --configuration=preview` + ); + expect(stripConsoleColors(result.stdout.replace(/\n|\s/g, ''))).toContain( + [ + 'Targets to be executed:', + `${project}:build.client:preview`, + `${project}:build.ssr:preview`, + ] + .join('') + .replace(/\n|\s/g, '') + ); + expect(result.stdout).toContain( + `Successfully ran target build for project ${project}` + ); + }, 200000); + it('should serve application in dev mode with custom port', async () => { const port = 4212; const p = await runCommandUntil( diff --git a/e2e/qwik-nx-e2e/tests/qwik-nx-vite.spec.ts b/e2e/qwik-nx-e2e/tests/qwik-nx-vite.spec.ts index 53ad48de..4a2d9c7c 100644 --- a/e2e/qwik-nx-e2e/tests/qwik-nx-vite.spec.ts +++ b/e2e/qwik-nx-e2e/tests/qwik-nx-vite.spec.ts @@ -106,9 +106,9 @@ describe('qwikNxVite plugin e2e', () => { }, 200000); it('should be able to successfully build the application', async () => { - const result = await runNxCommandAsync(`build-ssr ${project}`); + const result = await runNxCommandAsync(`build ${project}`); expect(result.stdout).toContain( - `Successfully ran target build-ssr for project ${project}` + `Successfully ran target build for project ${project}` ); expect(() => checkFilesExist(`dist/apps/${project}/client/q-manifest.json`) diff --git a/e2e/utils/index.ts b/e2e/utils/index.ts index 724ec269..98ac0c0c 100644 --- a/e2e/utils/index.ts +++ b/e2e/utils/index.ts @@ -186,7 +186,7 @@ export function getEnvironmentVariables() { * @param log * @returns */ -function stripConsoleColors(log: string): string { +export function stripConsoleColors(log: string): string { return log.replace( /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '' diff --git a/packages/qwik-nx/.eslintrc.json b/packages/qwik-nx/.eslintrc.json index 556554a7..f300fc16 100644 --- a/packages/qwik-nx/.eslintrc.json +++ b/packages/qwik-nx/.eslintrc.json @@ -15,7 +15,12 @@ "rules": {} }, { - "files": ["./package.json", "./generators.json", "./executors.json"], + "files": [ + "./package.json", + "./generators.json", + "./executors.json", + "./migrations.json" + ], "parser": "jsonc-eslint-parser", "rules": { "@nrwl/nx/nx-plugin-checks": "error" diff --git a/packages/qwik-nx/executors.json b/packages/qwik-nx/executors.json index b64f99d9..b92c873c 100644 --- a/packages/qwik-nx/executors.json +++ b/packages/qwik-nx/executors.json @@ -1,4 +1,10 @@ { "$schema": "http://json-schema.org/schema", - "executors": {} + "executors": { + "build": { + "implementation": "./src/executors/build/executor", + "schema": "./src/executors/build/schema.json", + "description": "build executor" + } + } } diff --git a/packages/qwik-nx/migrations.json b/packages/qwik-nx/migrations.json new file mode 100644 index 00000000..9395746e --- /dev/null +++ b/packages/qwik-nx/migrations.json @@ -0,0 +1,10 @@ +{ + "generators": { + "switch-to-qwik-nx:build-executor": { + "version": "0.11.0", + "description": "switch-to-qwik-nx:build-executor", + "cli": "nx", + "implementation": "./src/migrations/switch-to-qwik-nx:build-executor/switch-to-qwik-nx:build-executor" + } + } +} diff --git a/packages/qwik-nx/package.json b/packages/qwik-nx/package.json index 65d972f7..6253e589 100644 --- a/packages/qwik-nx/package.json +++ b/packages/qwik-nx/package.json @@ -28,5 +28,8 @@ "vitest": "^0.25.0", "@playwright/test": "^1.30.0", "undici": "^5.18.0" + }, + "nx-migrations": { + "migrations": "./migrations.json" } } diff --git a/packages/qwik-nx/project.json b/packages/qwik-nx/project.json index d1ab690b..56da0f4f 100644 --- a/packages/qwik-nx/project.json +++ b/packages/qwik-nx/project.json @@ -37,6 +37,11 @@ "input": "./packages/qwik-nx", "glob": "executors.json", "output": "." + }, + { + "input": "./packages/qwik-nx", + "glob": "migrations.json", + "output": "." } ] } @@ -49,7 +54,8 @@ "packages/qwik-nx/**/*.ts", "packages/qwik-nx/generators.json", "packages/qwik-nx/executors.json", - "packages/qwik-nx/package.json" + "packages/qwik-nx/package.json", + "packages/qwik-nx/migrations.json" ] } }, diff --git a/packages/qwik-nx/src/executors/build/executor.spec.ts b/packages/qwik-nx/src/executors/build/executor.spec.ts new file mode 100644 index 00000000..6e0d5618 --- /dev/null +++ b/packages/qwik-nx/src/executors/build/executor.spec.ts @@ -0,0 +1,50 @@ +import { BuildExecutorSchema } from './schema'; +import executor from './executor'; +import { ExecutorContext, runExecutor, Target } from '@nrwl/devkit'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const devkit: { runExecutor: typeof runExecutor } = require('@nrwl/devkit'); + +describe('Build Executor', () => { + let runExecutorPayloads: Target[] = []; + + jest.spyOn(devkit, 'runExecutor').mockImplementation((target: Target) => + Promise.resolve( + (async function* () { + runExecutorPayloads.push(target); + yield { success: true, target }; // yielding target for debugging purposes + })() + ) + ); + + afterEach(() => { + runExecutorPayloads = []; + }); + + it('should execute targets sequentially', async () => { + const context = { + root: '/root', + projectName: 'my-app', + targetName: 'build', + configurationName: 'production', + } as ExecutorContext; + + const options: BuildExecutorSchema = { + runSequence: ['my-app:target1:development', 'my-app:target2'], + }; + const iterable = executor(options, context); + await iterable.next(); + expect(runExecutorPayloads.map((p) => p.target)).toEqual(['target1']); + await iterable.next(); + expect(runExecutorPayloads.map((p) => p.target)).toEqual([ + 'target1', + 'target2', + ]); + const result = await iterable.next(); + expect(runExecutorPayloads).toEqual([ + { project: 'my-app', target: 'target1', configuration: 'development' }, + { project: 'my-app', target: 'target2', configuration: 'production' }, + ]); + expect(result.done).toEqual(true); + }); +}); diff --git a/packages/qwik-nx/src/executors/build/executor.ts b/packages/qwik-nx/src/executors/build/executor.ts new file mode 100644 index 00000000..3d7ae13b --- /dev/null +++ b/packages/qwik-nx/src/executors/build/executor.ts @@ -0,0 +1,41 @@ +import { + ExecutorContext, + output, + parseTargetString, + runExecutor, + targetToTargetString, +} from '@nrwl/devkit'; +import { BuildExecutorSchema } from './schema'; +import * as chalk from 'chalk'; + +export default async function* runBuildExecutor( + options: BuildExecutorSchema, + context: ExecutorContext +) { + const configs = options.runSequence.map((target) => { + const cfg = parseTargetString(target, context.projectGraph!); + cfg.configuration ??= context.configurationName; + return cfg; + }); + + output.log({ + title: `Building the ${context.projectName} project`, + bodyLines: [ + '\nTargets to be executed:', + ...configs.map((t) => chalk.dim(targetToTargetString(t))), + ], + }); + + for (const target of configs) { + const step = await runExecutor(target, {}, context); + + for await (const result of step) { + if (!result.success) { + return result; + } + yield { + success: true, + }; + } + } +} diff --git a/packages/qwik-nx/src/executors/build/schema.d.ts b/packages/qwik-nx/src/executors/build/schema.d.ts new file mode 100644 index 00000000..4f37faed --- /dev/null +++ b/packages/qwik-nx/src/executors/build/schema.d.ts @@ -0,0 +1,3 @@ +export interface BuildExecutorSchema { + runSequence: string[]; +} diff --git a/packages/qwik-nx/src/executors/build/schema.json b/packages/qwik-nx/src/executors/build/schema.json new file mode 100644 index 00000000..98cac265 --- /dev/null +++ b/packages/qwik-nx/src/executors/build/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/schema", + "version": 2, + "cli": "nx", + "title": "Build executor", + "description": "", + "type": "object", + "properties": { + "runSequence": { + "description": "An array of targets to be executed in a sequence", + "type": "array", + "items": { + "type": "string", + "x-completion-type": "projectTarget" + } + } + }, + "required": ["runSequence"] +} diff --git a/packages/qwik-nx/src/generators/application/utils/get-qwik-application-project-params.ts b/packages/qwik-nx/src/generators/application/utils/get-qwik-application-project-params.ts index 0d285051..61099f2f 100644 --- a/packages/qwik-nx/src/generators/application/utils/get-qwik-application-project-params.ts +++ b/packages/qwik-nx/src/generators/application/utils/get-qwik-application-project-params.ts @@ -11,16 +11,35 @@ export function getQwikApplicationProjectTargets( ): Record { return { build: getBuildTarget(params), - 'build-ssr': getBuildSsrTarget(params), + 'build.client': getBuildClientTarget(params), + 'build.ssr': getBuildSsrTarget(params), preview: getPreviewTarget(params), test: getTestTarget(params), serve: getServeTarget(params), - serveDebug: getServeDebugTarget(params), + 'serve.debug': getServeDebugTarget(params), }; } function getBuildTarget( params: UpdateQwikAppConfigurationParams +): TargetConfiguration { + return { + executor: 'qwik-nx:build', + options: { + runSequence: [ + `${params.projectName}:build.client`, + `${params.projectName}:build.ssr`, + ], + outputPath: `dist/${params.projectRoot}`, + }, + configurations: { + preview: {}, + }, + }; +} + +function getBuildClientTarget( + params: UpdateQwikAppConfigurationParams ): TargetConfiguration { return { executor: '@nrwl/vite:build', @@ -46,7 +65,6 @@ function getBuildSsrTarget( mode: 'production', }, }, - dependsOn: ['build'], }; } @@ -59,7 +77,7 @@ function getPreviewTarget( command: 'vite preview', cwd: `${params.projectRoot}`, }, - dependsOn: ['build-ssr'], + dependsOn: ['build'], }; } function getTestTarget( @@ -80,21 +98,10 @@ function getServeTarget( ): TargetConfiguration { return { executor: '@nrwl/vite:dev-server', - defaultConfiguration: 'development', options: { - buildTarget: `${params.projectName}:build`, + buildTarget: `${params.projectName}:build.client`, mode: 'ssr', }, - configurations: { - development: { - buildTarget: `${params.projectName}:build:development`, - hmr: true, - }, - production: { - buildTarget: `${params.projectName}:build:production`, - hmr: false, - }, - }, }; } @@ -104,7 +111,7 @@ function getServeDebugTarget( return { executor: 'nx:run-commands', options: { - command: `node --inspect-brk ${params.offsetFromRoot}/node_modules/vite/bin/vite.js --mode ssr --force`, + command: `node --inspect-brk ${params.offsetFromRoot}node_modules/vite/bin/vite.js --mode ssr --force`, cwd: params.projectRoot, }, }; diff --git a/packages/qwik-nx/src/generators/integrations/cloudflare-pages-integration/generator.spec.ts b/packages/qwik-nx/src/generators/integrations/cloudflare-pages-integration/generator.spec.ts index 8f97df85..74146306 100644 --- a/packages/qwik-nx/src/generators/integrations/cloudflare-pages-integration/generator.spec.ts +++ b/packages/qwik-nx/src/generators/integrations/cloudflare-pages-integration/generator.spec.ts @@ -35,9 +35,7 @@ describe('cloudflare-pages-integration generator', () => { it('should add required targets', async () => { await cloudflarePagesIntegrationGenerator(appTree, options); const config = readProjectConfiguration(appTree, projectName); - expect( - config.targets!['build-ssr'].configurations!['cloudflare-pages'] - ).toEqual({ + expect(config.targets!['build.ssr'].configurations!['production']).toEqual({ configFile: `apps/${projectName}/adaptors/cloudflare-pages/vite.config.ts`, }); expect(config.targets!['deploy']).toEqual({ @@ -45,19 +43,64 @@ describe('cloudflare-pages-integration generator', () => { options: { dist: `dist/apps/${projectName}/client`, }, - dependsOn: ['build-ssr-cloudflare-pages'], + dependsOn: ['build-cloudflare'], }); - expect(config.targets!['preview-cloudflare-pages']).toEqual({ + expect(config.targets!['preview-cloudflare']).toEqual({ executor: '@k11r/nx-cloudflare-wrangler:serve-page', options: { dist: `dist/apps/${projectName}/client`, }, - dependsOn: ['build-ssr-cloudflare-pages'], + dependsOn: ['build-cloudflare'], + }); + expect(config.targets!['build-cloudflare']).toEqual({ + executor: 'nx:run-commands', + options: { + command: `npx nx run ${projectName}:build:production`, + }, + }); + }); + + it('should use other target name if deploy target is already defined', async () => { + const configBefore = readProjectConfiguration(appTree, projectName); + configBefore.targets!['deploy'] = { executor: 'nx:noop' }; + updateProjectConfiguration(appTree, projectName, configBefore); + + await cloudflarePagesIntegrationGenerator(appTree, options); + + const config = readProjectConfiguration(appTree, projectName); + expect(config.targets!['build.ssr'].configurations!['production']).toEqual({ + configFile: `apps/${projectName}/adaptors/cloudflare-pages/vite.config.ts`, + }); + expect(config.targets!['deploy']).toEqual({ executor: 'nx:noop' }); + expect(config.targets!['deploy.cloudflare'].executor).toEqual( + '@k11r/nx-cloudflare-wrangler:deploy-page' + ); + }); + + it('should use the name of the integration if configuration name "production" is already defined', async () => { + const configBefore = readProjectConfiguration(appTree, projectName); + configBefore.targets!['build.ssr'].configurations!['production'] = {}; + updateProjectConfiguration(appTree, projectName, configBefore); + + await cloudflarePagesIntegrationGenerator(appTree, options); + + const config = readProjectConfiguration(appTree, projectName); + expect( + config.targets!['build'].configurations!['production'] + ).toBeUndefined(); + expect(config.targets!['build.ssr'].configurations!['production']).toEqual( + {} + ); + expect(config.targets!['build.ssr'].configurations!['cloudflare']).toEqual({ + configFile: `apps/${projectName}/adaptors/cloudflare-pages/vite.config.ts`, }); - expect(config.targets!['build-ssr-cloudflare-pages']).toEqual({ + expect(config.targets!['deploy'].executor).toEqual( + '@k11r/nx-cloudflare-wrangler:deploy-page' + ); + expect(config.targets!['build-cloudflare']).toEqual({ executor: 'nx:run-commands', options: { - command: `npx nx run ${projectName}:build-ssr:cloudflare-pages`, + command: `npx nx run ${projectName}:build:cloudflare`, }, }); }); @@ -70,17 +113,6 @@ describe('cloudflare-pages-integration generator', () => { }); describe('should throw if project configuration does not meet the expectations', () => { - it('deploy target is already defined', async () => { - const config = readProjectConfiguration(appTree, projectName); - config.targets!['deploy'] = { executor: 'nx:noop' }; - updateProjectConfiguration(appTree, projectName, config); - - expect( - cloudflarePagesIntegrationGenerator(appTree, options) - ).rejects.toThrow( - `"deploy" target has already been configured for ${options.project}` - ); - }); it('project is not an application', async () => { const config = readProjectConfiguration(appTree, projectName); config.projectType = 'library'; @@ -95,14 +127,12 @@ describe('cloudflare-pages-integration generator', () => { it('project does not have Qwik\'s "build-ssr" target', async () => { const config = readProjectConfiguration(appTree, projectName); - delete config.targets!['build-ssr']; + config.targets!.build.executor = 'nx:run-commands'; updateProjectConfiguration(appTree, projectName, config); - expect( + await expect( cloudflarePagesIntegrationGenerator(appTree, options) - ).rejects.toThrow( - 'Cannot setup cloudflare integration for the given project.' - ); + ).rejects.toThrow('Project contains invalid configuration.'); }); }); }); diff --git a/packages/qwik-nx/src/generators/integrations/cloudflare-pages-integration/generator.ts b/packages/qwik-nx/src/generators/integrations/cloudflare-pages-integration/generator.ts index 428f969b..712df0ed 100644 --- a/packages/qwik-nx/src/generators/integrations/cloudflare-pages-integration/generator.ts +++ b/packages/qwik-nx/src/generators/integrations/cloudflare-pages-integration/generator.ts @@ -12,6 +12,10 @@ import { Tree, updateProjectConfiguration, } from '@nrwl/devkit'; +import { + getIntegrationConfigurationName, + IntegrationName, +} from '../../../utils/integration-configuration-name'; import { nxCloudflareWrangler, wranglerVersion } from '../../../utils/versions'; import { CloudflarePagesIntegrationGeneratorSchema } from './schema'; @@ -25,27 +29,37 @@ export async function cloudflarePagesIntegrationGenerator( options: CloudflarePagesIntegrationGeneratorSchema ) { const config = readProjectConfiguration(tree, options.project); - config.targets ??= {}; - - if (config.projectType !== 'application' || !config.targets['build-ssr']) { + if (config.projectType !== 'application') { throw new Error( 'Cannot setup cloudflare integration for the given project.' ); } - if (config.targets['deploy']) { + if (config.targets?.['build']?.executor !== 'qwik-nx:build') { throw new Error( - `"deploy" target has already been configured for ${options.project}` + 'Project contains invalid configuration. ' + + 'If you encounter this error within a Qwik project, make sure you have run necessary Nx migrations for qwik-nx plugin.' ); } + const configurationName = getIntegrationConfigurationName( + IntegrationName.Cloudflare, + config + ); + const deployTargetName = config.targets['deploy'] + ? 'deploy.cloudflare' + : 'deploy'; + const normalizedOptions = normalizeOptions(config); - (config.targets['build-ssr'].configurations ??= {})['cloudflare-pages'] = + (config.targets['build']?.configurations ?? {})[configurationName] = {}; + (config.targets['build.ssr'].configurations ??= {})[configurationName] = getBuildSSRTargetCloudflareConfiguration(normalizedOptions); - config.targets['deploy'] = getDeployTarget(normalizedOptions); - config.targets['preview-cloudflare-pages'] = + config.targets[deployTargetName] = getDeployTarget(normalizedOptions); + config.targets['preview-cloudflare'] = getCloudflarePreviewTarget(normalizedOptions); - config.targets['build-ssr-cloudflare-pages'] = - getIntermediateDependsOnTarget(normalizedOptions); + config.targets['build-cloudflare'] = getIntermediateDependsOnTarget( + normalizedOptions, + configurationName + ); updateProjectConfiguration(tree, options.project, config); @@ -70,7 +84,7 @@ function getDeployTarget(options: NormalizedOptions): TargetConfiguration { options: { dist: `dist/${options.projectConfig.root}/client`, }, - dependsOn: ['build-ssr-cloudflare-pages'], + dependsOn: ['build-cloudflare'], }; } @@ -82,18 +96,19 @@ function getCloudflarePreviewTarget( options: { dist: `dist/${options.projectConfig.root}/client`, }, - dependsOn: ['build-ssr-cloudflare-pages'], + dependsOn: ['build-cloudflare'], }; } /** Currently it's not possible to depend on a target with a specific configuration, that's why intermediate one is required */ function getIntermediateDependsOnTarget( - options: NormalizedOptions + options: NormalizedOptions, + configurationName: string ): TargetConfiguration { return { executor: 'nx:run-commands', options: { - command: `npx nx run ${options.projectConfig.name}:build-ssr:cloudflare-pages`, + command: `npx nx run ${options.projectConfig.name}:build:${configurationName}`, }, }; } diff --git a/packages/qwik-nx/src/migrations/switch-to-qwik-nx:build-executor/switch-to-qwik-nx:build-executor.spec.ts b/packages/qwik-nx/src/migrations/switch-to-qwik-nx:build-executor/switch-to-qwik-nx:build-executor.spec.ts new file mode 100644 index 00000000..73234711 --- /dev/null +++ b/packages/qwik-nx/src/migrations/switch-to-qwik-nx:build-executor/switch-to-qwik-nx:build-executor.spec.ts @@ -0,0 +1,397 @@ +import { + addProjectConfiguration, + ProjectConfiguration, + readProjectConfiguration, + Tree, +} from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import migrate from './switch-to-qwik-nx:build-executor'; + +describe('Use new "qwik-nx:build" executor in qwik apps', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + }); + + it('should update targets in a standard project.json', async () => { + addProjectConfiguration( + tree, + 'myapp', + getSampleProjectJson().oldFormatWithCF + ); + + await migrate(tree); + + expect(readProjectConfiguration(tree, 'myapp')).toEqual( + getSampleProjectJson().newFormatWithCF + ); + }); +}); + +function getSampleProjectJson() { + return { + oldFormat: { + root: 'apps/myapp', + name: 'myapp', + $schema: '../../node_modules/nx/schemas/project-schema.json', + projectType: 'application', + sourceRoot: 'apps/myapp/src', + targets: { + build: { + executor: '@nrwl/vite:build', + options: { + outputPath: 'dist/apps/myapp', + configFile: 'apps/myapp/vite.config.ts', + }, + }, + 'build-ssr': { + executor: '@nrwl/vite:build', + defaultConfiguration: 'preview', + options: { + outputPath: 'dist/apps/myapp', + }, + configurations: { + preview: { + ssr: 'src/entry.preview.tsx', + mode: 'production', + }, + }, + dependsOn: ['build'], + }, + preview: { + executor: 'nx:run-commands', + options: { + command: 'vite preview', + cwd: 'apps/myapp', + }, + dependsOn: ['build-ssr'], + }, + test: { + executor: '@nrwl/vite:test', + outputs: ['../../coverage/apps/myapp'], + options: { + passWithNoTests: true, + reportsDirectory: '../../coverage/apps/myapp', + }, + }, + serve: { + executor: '@nrwl/vite:dev-server', + defaultConfiguration: 'development', + options: { + buildTarget: 'myapp:build', + mode: 'ssr', + }, + configurations: { + development: { + buildTarget: 'myapp:build:development', + hmr: true, + }, + production: { + buildTarget: 'myapp:build:production', + hmr: false, + }, + }, + }, + serveDebug: { + executor: 'nx:run-commands', + options: { + command: + 'node --inspect-brk ../../node_modules/vite/bin/vite.js --mode ssr --force', + cwd: 'apps/myapp', + }, + }, + lint: { + executor: '@nrwl/linter:eslint', + outputs: ['{options.outputFile}'], + options: { + lintFilePatterns: ['apps/myapp/**/*.{ts,tsx,js,jsx}'], + }, + }, + }, + tags: [], + } as ProjectConfiguration, + newFormat: { + root: 'apps/myapp', + name: 'myapp', + $schema: '../../node_modules/nx/schemas/project-schema.json', + projectType: 'application', + sourceRoot: 'apps/myapp/src', + targets: { + build: { + executor: 'qwik-nx:build', + options: { + runSequence: ['myapp:build.client', 'myapp:build.ssr'], + outputPath: 'dist/apps/myapp', + }, + }, + 'build.client': { + executor: '@nrwl/vite:build', + options: { + outputPath: 'dist/apps/myapp', + configFile: 'apps/myapp/vite.config.ts', + }, + }, + 'build.ssr': { + executor: '@nrwl/vite:build', + defaultConfiguration: 'preview', + options: { + outputPath: 'dist/apps/myapp', + }, + configurations: { + preview: { + ssr: 'src/entry.preview.tsx', + mode: 'production', + }, + }, + dependsOn: [], + }, + preview: { + executor: 'nx:run-commands', + options: { + command: 'vite preview', + cwd: 'apps/myapp', + }, + dependsOn: ['build'], + }, + test: { + executor: '@nrwl/vite:test', + outputs: ['../../coverage/apps/myapp'], + options: { + passWithNoTests: true, + reportsDirectory: '../../coverage/apps/myapp', + }, + }, + serve: { + executor: '@nrwl/vite:dev-server', + defaultConfiguration: 'development', + options: { + buildTarget: 'myapp:build.client', + mode: 'ssr', + }, + configurations: { + development: { + buildTarget: 'myapp:build.client:development', + hmr: true, + }, + production: { + buildTarget: 'myapp:build.client:production', + hmr: false, + }, + }, + }, + 'serve.debug': { + executor: 'nx:run-commands', + options: { + command: + 'node --inspect-brk ../../node_modules/vite/bin/vite.js --mode ssr --force', + cwd: 'apps/myapp', + }, + }, + lint: { + executor: '@nrwl/linter:eslint', + outputs: ['{options.outputFile}'], + options: { + lintFilePatterns: ['apps/myapp/**/*.{ts,tsx,js,jsx}'], + }, + }, + }, + tags: [], + } as ProjectConfiguration, + oldFormatWithCF: { + root: 'apps/myapp', + name: 'myapp', + $schema: '../../node_modules/nx/schemas/project-schema.json', + projectType: 'application', + sourceRoot: 'apps/myapp/src', + targets: { + build: { + executor: '@nrwl/vite:build', + options: { + outputPath: 'dist/apps/myapp', + configFile: 'apps/myapp/vite.config.ts', + }, + }, + 'build-ssr': { + executor: '@nrwl/vite:build', + defaultConfiguration: 'preview', + options: { + outputPath: 'dist/apps/myapp', + }, + configurations: { + preview: { + ssr: 'src/entry.preview.tsx', + mode: 'production', + }, + 'cloudflare-pages': { + configFile: 'apps/myapp/adaptors/cloudflare-pages/vite.config.ts', + }, + }, + dependsOn: ['build'], + }, + preview: { + executor: 'nx:run-commands', + options: { + command: 'vite preview', + cwd: 'apps/myapp', + }, + dependsOn: ['build-ssr'], + }, + test: { + executor: '@nrwl/vite:test', + outputs: ['../../coverage/apps/myapp'], + options: { + passWithNoTests: true, + reportsDirectory: '../../coverage/apps/myapp', + }, + }, + serve: { + executor: '@nrwl/vite:dev-server', + options: { + buildTarget: 'myapp:build', + mode: 'ssr', + }, + }, + serveDebug: { + executor: 'nx:run-commands', + options: { + command: + 'node --inspect-brk ../../node_modules/vite/bin/vite.js --mode ssr --force', + cwd: 'apps/myapp', + }, + }, + lint: { + executor: '@nrwl/linter:eslint', + outputs: ['{options.outputFile}'], + options: { + lintFilePatterns: ['apps/myapp/**/*.{ts,tsx,js,jsx}'], + }, + }, + deploy: { + executor: '@k11r/nx-cloudflare-wrangler:deploy-page', + options: { + dist: 'dist/apps/myapp/client', + }, + dependsOn: ['build-ssr-cloudflare-pages'], + }, + 'preview-cloudflare-pages': { + executor: '@k11r/nx-cloudflare-wrangler:serve-page', + options: { + dist: 'dist/apps/myapp/client', + }, + dependsOn: ['build-ssr-cloudflare-pages'], + }, + 'build-ssr-cloudflare-pages': { + executor: 'nx:run-commands', + options: { + command: 'npx nx run myapp:build-ssr:cloudflare-pages', + }, + }, + }, + tags: [], + } as ProjectConfiguration, + newFormatWithCF: { + root: 'apps/myapp', + name: 'myapp', + $schema: '../../node_modules/nx/schemas/project-schema.json', + projectType: 'application', + sourceRoot: 'apps/myapp/src', + targets: { + build: { + executor: 'qwik-nx:build', + options: { + runSequence: ['myapp:build.client', 'myapp:build.ssr'], + outputPath: 'dist/apps/myapp', + }, + configurations: { + 'cloudflare-pages': {}, + preview: {}, + }, + }, + 'build.client': { + executor: '@nrwl/vite:build', + options: { + outputPath: 'dist/apps/myapp', + configFile: 'apps/myapp/vite.config.ts', + }, + }, + 'build.ssr': { + executor: '@nrwl/vite:build', + defaultConfiguration: 'preview', + options: { + outputPath: 'dist/apps/myapp', + }, + configurations: { + preview: { + ssr: 'src/entry.preview.tsx', + mode: 'production', + }, + 'cloudflare-pages': { + configFile: 'apps/myapp/adaptors/cloudflare-pages/vite.config.ts', + }, + }, + dependsOn: [], + }, + preview: { + executor: 'nx:run-commands', + options: { + command: 'vite preview', + cwd: 'apps/myapp', + }, + dependsOn: ['build'], + }, + test: { + executor: '@nrwl/vite:test', + outputs: ['../../coverage/apps/myapp'], + options: { + passWithNoTests: true, + reportsDirectory: '../../coverage/apps/myapp', + }, + }, + serve: { + executor: '@nrwl/vite:dev-server', + options: { + buildTarget: 'myapp:build.client', + mode: 'ssr', + }, + }, + 'serve.debug': { + executor: 'nx:run-commands', + options: { + command: + 'node --inspect-brk ../../node_modules/vite/bin/vite.js --mode ssr --force', + cwd: 'apps/myapp', + }, + }, + lint: { + executor: '@nrwl/linter:eslint', + outputs: ['{options.outputFile}'], + options: { + lintFilePatterns: ['apps/myapp/**/*.{ts,tsx,js,jsx}'], + }, + }, + deploy: { + executor: '@k11r/nx-cloudflare-wrangler:deploy-page', + options: { + dist: 'dist/apps/myapp/client', + }, + dependsOn: ['build-ssr-cloudflare-pages'], + }, + 'preview-cloudflare-pages': { + executor: '@k11r/nx-cloudflare-wrangler:serve-page', + options: { + dist: 'dist/apps/myapp/client', + }, + dependsOn: ['build-ssr-cloudflare-pages'], + }, + 'build-ssr-cloudflare-pages': { + executor: 'nx:run-commands', + options: { + command: 'npx nx run myapp:build:cloudflare-pages', + }, + }, + }, + tags: [], + } as ProjectConfiguration, + }; +} diff --git a/packages/qwik-nx/src/migrations/switch-to-qwik-nx:build-executor/switch-to-qwik-nx:build-executor.ts b/packages/qwik-nx/src/migrations/switch-to-qwik-nx:build-executor/switch-to-qwik-nx:build-executor.ts new file mode 100644 index 00000000..309f56f7 --- /dev/null +++ b/packages/qwik-nx/src/migrations/switch-to-qwik-nx:build-executor/switch-to-qwik-nx:build-executor.ts @@ -0,0 +1,101 @@ +import { + getProjects, + ProjectConfiguration, + TargetConfiguration, + Tree, + updateProjectConfiguration, +} from '@nrwl/devkit'; + +export default function update(host: Tree) { + const projects = getProjects(host); + + projects.forEach((config, name) => { + if (isQwikNxProject(config)) { + // rename targets + config.targets['build.client'] = config.targets['build']; + config.targets['build.ssr'] = config.targets['build-ssr']; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete config.targets['build-ssr']; + if (config.targets['serveDebug']) { + config.targets['serve.debug'] = config.targets['serveDebug']; + delete config.targets['serveDebug']; + } + + // add new build target + config.targets.build = { + executor: 'qwik-nx:build', + options: { + runSequence: [`${name}:build.client`, `${name}:build.ssr`], + outputPath: + config.targets['build.client'].options['outputPath'] ?? + `dist/${config.root}`, + }, + configurations: { preview: {} }, + }; + + // update buildTarget for the serve target and its configurations + const serveTarget = config.targets['serve']; + if (serveTarget) { + // using "split" because target can be specified w\ or w\o configuration + serveTarget.options['buildTarget'] = config.targets['serve'].options[ + 'buildTarget' + ] + ?.split(':') + .map((part: string) => (part === 'build' ? 'build.client' : part)) + .join(':'); + Object.keys(serveTarget.configurations ?? {}).forEach( + (configurationName) => { + const cfg = serveTarget.configurations![configurationName]; + cfg.buildTarget = cfg.buildTarget + ?.split(':') + .map((part: string) => (part === 'build' ? 'build.client' : part)) + .join(':'); + } + ); + } + + // update dependsOn + config.targets['build.ssr'].dependsOn = config.targets[ + 'build.ssr' + ].dependsOn?.filter((target) => target !== 'build'); + config.targets['preview'].dependsOn = config.targets[ + 'preview' + ].dependsOn?.map((target) => (target === 'build-ssr' ? 'build' : target)); + + // update intermediate target for cloudflare if it exists + const cfTargetOptions = + config.targets['build-ssr-cloudflare-pages']?.options; + if (cfTargetOptions?.command) { + cfTargetOptions.command = cfTargetOptions.command.replace( + 'build-ssr:cloudflare-pages', + 'build:cloudflare-pages' + ); + + // add configuration in the build command for cloudflare pages if it does not exist + (config.targets.build.configurations ??= {})['cloudflare-pages'] ??= {}; + } + + updateProjectConfiguration(host, name, config); + } + }); +} + +function isQwikNxProject( + config: ProjectConfiguration +): config is OldQwikNxConfiguration { + if (config.targets?.['build']?.executor !== '@nrwl/vite:build') { + return false; + } + if (config.targets['build-ssr']?.executor !== '@nrwl/vite:build') { + return false; + } + if (!config.targets['preview']) { + return false; + } + return true; +} + +type OldQwikNxConfiguration = ProjectConfiguration & { + targets: Record<'build' | 'build-ssr' | 'preview', TargetConfiguration>; +}; diff --git a/packages/qwik-nx/src/utils/integration-configuration-name.ts b/packages/qwik-nx/src/utils/integration-configuration-name.ts new file mode 100644 index 00000000..d63f3dfe --- /dev/null +++ b/packages/qwik-nx/src/utils/integration-configuration-name.ts @@ -0,0 +1,24 @@ +import { ProjectConfiguration } from '@nrwl/devkit'; + +export enum IntegrationName { + Cloudflare = 'cloudflare', +} + +/** + * If there're no integrations, the one that is being added will get a "production" mode as its configuration. + * Otherwise the respective configuration name will be used as a configuration + */ +export function getIntegrationConfigurationName( + integration: IntegrationName, + project: ProjectConfiguration +): string { + const buildTargetConfiguration = + !!project.targets?.build?.configurations?.production; + const buildSsrTargetConfiguration = + !!project.targets?.['build.ssr']?.configurations?.production; + + if (buildTargetConfiguration || buildSsrTargetConfiguration) { + return integration; + } + return 'production'; +}