Skip to content

Commit 997caa3

Browse files
feat: cloudflare pages integration (#48)
1 parent b89b642 commit 997caa3

File tree

11 files changed

+315
-0
lines changed

11 files changed

+315
-0
lines changed

packages/qwik-nx/generators.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@
4949
"factory": "./src/generators/e2e-project/generator",
5050
"schema": "./src/generators/e2e-project/schema.json",
5151
"description": "Create an E2E app for a Qwik app"
52+
},
53+
"cloudflare-pages-integration": {
54+
"factory": "./src/generators/integrations/cloudflare-pages-integration/generator",
55+
"schema": "./src/generators/integrations/cloudflare-pages-integration/schema.json",
56+
"description": "Qwik City Cloudflare Pages adaptor allows you to connect Qwik City to Cloudflare Pages"
5257
}
5358
}
5459
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { cloudflarePagesAdaptor } from '@builder.io/qwik-city/adaptors/cloudflare-pages/vite';
2+
import { extendConfig } from '@builder.io/qwik-city/vite';
3+
import baseConfig from '../../vite.config';
4+
5+
export default extendConfig(baseConfig, () => {
6+
return {
7+
build: {
8+
ssr: true,
9+
rollupOptions: {
10+
input: ['<%= projectRoot %>/src/entry.cloudflare-pages.tsx', '@qwik-city-plan'],
11+
},
12+
},
13+
plugins: [
14+
cloudflarePagesAdaptor({
15+
staticGenerate: true,
16+
}),
17+
],
18+
};
19+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// eslint-disable-next-line
2+
// @ts-ignore
3+
4+
// Cloudflare Pages Functions
5+
// https://developers.cloudflare.com/pages/platform/functions/
6+
export { onRequest } from '<%= offsetFromRoot %>../dist/<%= projectRoot %>/server/entry.cloudflare-pages';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# https://developers.cloudflare.com/pages/platform/headers/
2+
3+
/build/*
4+
Cache-Control: public, max-age=31536000, s-maxage=31536000, immutable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# https://developers.cloudflare.com/pages/platform/redirects/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* WHAT IS THIS FILE?
3+
*
4+
* It's the entry point for cloudflare-pages when building for production.
5+
*
6+
* Learn more about the cloudflare integration here:
7+
* - https://qwik.builder.io/integrations/deployments/cloudflare-pages/
8+
*
9+
*/
10+
import { createQwikCity } from '@builder.io/qwik-city/middleware/cloudflare-pages';
11+
import qwikCityPlan from '@qwik-city-plan';
12+
import render from './entry.ssr';
13+
14+
const onRequest = createQwikCity({ render, qwikCityPlan });
15+
16+
export { onRequest };
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
2+
import {
3+
Tree,
4+
readProjectConfiguration,
5+
readJson,
6+
updateProjectConfiguration,
7+
} from '@nrwl/devkit';
8+
9+
import generator from './generator';
10+
import applicationGenerator from './../../application/generator';
11+
import { CloudflarePagesIntegrationGeneratorSchema } from './schema';
12+
import { Linter } from '@nrwl/linter';
13+
14+
describe('cloudflare-pages-integration generator', () => {
15+
let appTree: Tree;
16+
const projectName = 'test-project';
17+
const options: CloudflarePagesIntegrationGeneratorSchema = {
18+
project: projectName,
19+
};
20+
21+
beforeEach(() => {
22+
appTree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
23+
24+
applicationGenerator(appTree, {
25+
name: projectName,
26+
e2eTestRunner: 'none',
27+
linter: Linter.None,
28+
skipFormat: false,
29+
strict: true,
30+
style: 'css',
31+
unitTestRunner: 'none',
32+
});
33+
});
34+
35+
it('should add required targets', async () => {
36+
await generator(appTree, options);
37+
const config = readProjectConfiguration(appTree, projectName);
38+
expect(
39+
config.targets['build-ssr'].configurations['cloudflare-pages']
40+
).toEqual({
41+
configFile: `apps/${projectName}/adaptors/cloudflare-pages/vite.config.ts`,
42+
});
43+
expect(config.targets['deploy']).toEqual({
44+
executor: '@k11r/nx-cloudflare-wrangler:deploy-page',
45+
options: {
46+
dist: `dist/apps/${projectName}/client`,
47+
},
48+
dependsOn: ['build-ssr-cloudflare-pages'],
49+
});
50+
expect(config.targets['preview-cloudflare-pages']).toEqual({
51+
executor: '@k11r/nx-cloudflare-wrangler:serve-page',
52+
options: {
53+
dist: `dist/apps/${projectName}/client`,
54+
},
55+
dependsOn: ['build-ssr-cloudflare-pages'],
56+
});
57+
expect(config.targets['build-ssr-cloudflare-pages']).toEqual({
58+
executor: 'nx:run-commands',
59+
options: {
60+
command: `npx nx run ${projectName}:build-ssr:cloudflare-pages`,
61+
},
62+
});
63+
});
64+
65+
it('should add required dependencies', async () => {
66+
await generator(appTree, options);
67+
const { devDependencies } = readJson(appTree, 'package.json');
68+
expect(devDependencies['wrangler']).toBeDefined();
69+
expect(devDependencies['@k11r/nx-cloudflare-wrangler']).toBeDefined();
70+
});
71+
72+
describe('should throw if project configuration does not meet the expectations', () => {
73+
it('deploy target is already defined', async () => {
74+
const config = readProjectConfiguration(appTree, projectName);
75+
config.targets['deploy'] = { executor: 'nx:noop' };
76+
updateProjectConfiguration(appTree, projectName, config);
77+
78+
expect(generator(appTree, options)).rejects.toThrow(
79+
`"deploy" target has already been configured for ${options.project}`
80+
);
81+
});
82+
it('project is not an application', async () => {
83+
const config = readProjectConfiguration(appTree, projectName);
84+
config.projectType = 'library';
85+
updateProjectConfiguration(appTree, projectName, config);
86+
87+
expect(generator(appTree, options)).rejects.toThrow(
88+
'Cannot setup cloudflare integration for the given project.'
89+
);
90+
});
91+
92+
it('project does not have Qwik\'s "build-ssr" target', async () => {
93+
const config = readProjectConfiguration(appTree, projectName);
94+
delete config.targets['build-ssr'];
95+
updateProjectConfiguration(appTree, projectName, config);
96+
97+
expect(generator(appTree, options)).rejects.toThrow(
98+
'Cannot setup cloudflare integration for the given project.'
99+
);
100+
});
101+
});
102+
});
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import {
2+
addDependenciesToPackageJson,
3+
formatFiles,
4+
generateFiles,
5+
GeneratorCallback,
6+
joinPathFragments,
7+
names,
8+
offsetFromRoot,
9+
ProjectConfiguration,
10+
readProjectConfiguration,
11+
TargetConfiguration,
12+
Tree,
13+
updateProjectConfiguration,
14+
} from '@nrwl/devkit';
15+
import { nxCloudflareWrangler, wranglerVersion } from '../../../utils/versions';
16+
import { CloudflarePagesIntegrationGeneratorSchema } from './schema';
17+
18+
interface NormalizedOptions {
19+
offsetFromRoot: string;
20+
projectConfig: ProjectConfiguration;
21+
}
22+
23+
export default async function (
24+
tree: Tree,
25+
options: CloudflarePagesIntegrationGeneratorSchema
26+
) {
27+
const config = readProjectConfiguration(tree, options.project);
28+
if (config.projectType !== 'application' || !config.targets['build-ssr']) {
29+
throw new Error(
30+
'Cannot setup cloudflare integration for the given project.'
31+
);
32+
}
33+
if (config.targets['deploy']) {
34+
throw new Error(
35+
`"deploy" target has already been configured for ${options.project}`
36+
);
37+
}
38+
39+
const normalizedOptions = normalizeOptions(config);
40+
(config.targets['build-ssr'].configurations ??= {})['cloudflare-pages'] =
41+
getBuildSSRTargetCloudflareConfiguration(normalizedOptions);
42+
config.targets['deploy'] = getDeployTarget(normalizedOptions);
43+
config.targets['preview-cloudflare-pages'] =
44+
getCloudflarePreviewTarget(normalizedOptions);
45+
config.targets['build-ssr-cloudflare-pages'] =
46+
getIntermediateDependsOnTarget(normalizedOptions);
47+
48+
updateProjectConfiguration(tree, options.project, config);
49+
50+
addFiles(tree, normalizedOptions);
51+
52+
if (!options.skipFormat) {
53+
await formatFiles(tree);
54+
}
55+
56+
return addCloudflarePagesDependencies(tree);
57+
}
58+
59+
function getBuildSSRTargetCloudflareConfiguration(options: NormalizedOptions) {
60+
return {
61+
configFile: `${options.projectConfig.root}/adaptors/cloudflare-pages/vite.config.ts`,
62+
};
63+
}
64+
65+
function getDeployTarget(options: NormalizedOptions): TargetConfiguration {
66+
return {
67+
executor: '@k11r/nx-cloudflare-wrangler:deploy-page',
68+
options: {
69+
dist: `dist/${options.projectConfig.root}/client`,
70+
},
71+
dependsOn: ['build-ssr-cloudflare-pages'],
72+
};
73+
}
74+
75+
function getCloudflarePreviewTarget(
76+
options: NormalizedOptions
77+
): TargetConfiguration {
78+
return {
79+
executor: '@k11r/nx-cloudflare-wrangler:serve-page',
80+
options: {
81+
dist: `dist/${options.projectConfig.root}/client`,
82+
},
83+
dependsOn: ['build-ssr-cloudflare-pages'],
84+
};
85+
}
86+
87+
/** Currently it's not possible to depend on a target with a specific configuration, that's why intermediate one is required */
88+
function getIntermediateDependsOnTarget(
89+
options: NormalizedOptions
90+
): TargetConfiguration {
91+
return {
92+
executor: 'nx:run-commands',
93+
options: {
94+
command: `npx nx run ${options.projectConfig.name}:build-ssr:cloudflare-pages`,
95+
},
96+
};
97+
}
98+
99+
function normalizeOptions(
100+
projectConfig: ProjectConfiguration
101+
): NormalizedOptions {
102+
return {
103+
projectConfig,
104+
offsetFromRoot: offsetFromRoot(projectConfig.root),
105+
};
106+
}
107+
108+
function addFiles(tree: Tree, options: NormalizedOptions): void {
109+
const templateOptions = {
110+
...names(options.projectConfig.name),
111+
projectRoot: options.projectConfig.root,
112+
offsetFromRoot: options.offsetFromRoot,
113+
};
114+
generateFiles(
115+
tree,
116+
joinPathFragments(__dirname, 'files'),
117+
options.projectConfig.root,
118+
templateOptions
119+
);
120+
}
121+
122+
function addCloudflarePagesDependencies(tree: Tree): GeneratorCallback {
123+
return addDependenciesToPackageJson(
124+
tree,
125+
{},
126+
{
127+
wrangler: wranglerVersion,
128+
'@k11r/nx-cloudflare-wrangler': nxCloudflareWrangler,
129+
}
130+
);
131+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface CloudflarePagesIntegrationGeneratorSchema {
2+
project: string;
3+
skipFormat?: boolean;
4+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "http://json-schema.org/schema",
3+
"cli": "nx",
4+
"$id": "CloudflarePagesIntegration",
5+
"title": "",
6+
"type": "object",
7+
"properties": {
8+
"project": {
9+
"type": "string",
10+
"description": "Project for the integration to be added",
11+
"$default": {
12+
"$source": "argv",
13+
"index": 0
14+
}
15+
},
16+
"skipFormat": {
17+
"description": "Skip formatting files.",
18+
"type": "boolean",
19+
"default": false
20+
}
21+
},
22+
"required": ["project"]
23+
}

0 commit comments

Comments
 (0)