Skip to content

Commit 700b9ec

Browse files
feat(qwik-nx): deno integration (#181)
* feat(qwik-nx): deno integration * feat(integrations-deno): generat GAH on demand * fix(integrations-deno): fixed deno server root; added missing tests * docs: updated README.md on how to use deno integration generator * fix(deno-integration): using tree.write instead of fs.writeFile to respect --dry-run flag * fix(deno-integration): fixed pnpm-lock.yaml for the CI #181 --------- Co-authored-by: Dmitriy Stepanenko <[email protected]>
1 parent 2ac9032 commit 700b9ec

File tree

11 files changed

+543
-9
lines changed

11 files changed

+543
-9
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ nx generate qwik-nx:route
7373
nx generate qwik-nx:setup-tailwind
7474
```
7575

76+
### Setting up Deno integration
77+
78+
```
79+
nx generate qwik-nx:deno-integration
80+
```
81+
7682
## Migrations
7783

7884
This plugin supports Nx migrations and provides necessary version and code updates for Qwik. So instead of bumping plugin version manually in package.json it's recommended to run `nx migrate qwik-nx` command, that includes bumping the version of the qwik-nx plugin, related dependencies and running code migrations.

packages/qwik-nx/generators.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@
8484
"factory": "./src/generators/integrations/react-library/generator",
8585
"schema": "./src/generators/integrations/react-library/schema.json",
8686
"description": "Add qwikified React components as a separate library"
87+
},
88+
"deno-integration": {
89+
"factory": "./src/generators/integrations/deno/generator",
90+
"schema": "./src/generators/integrations/deno/schema.json",
91+
"description": "Qwik City Deno adaptor allows you to hook up Qwik City to a Deno server"
8792
}
8893
}
8994
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Deploy
2+
on:
3+
push:
4+
branches: master
5+
pull_request:
6+
branches: master
7+
8+
jobs:
9+
deploy:
10+
name: Deploy
11+
runs-on: ubuntu-latest
12+
permissions:
13+
id-token: write # Needed for auth with Deno Deploy
14+
contents: read # Needed to clone the repository
15+
16+
steps:
17+
- name: Clone repository
18+
uses: actions/checkout@v3
19+
20+
- name: Install Node.js
21+
uses: actions/setup-node@v3
22+
with:
23+
node-version: lts/*
24+
25+
- name: Build step
26+
run: npm install && npm run build # 📝 Update the build command(s)
27+
28+
- name: Upload to Deno Deploy
29+
uses: denoland/deployctl@v1
30+
with:
31+
project: "<%= denoProjectName %>"
32+
entrypoint: "https://deno.land/std/http/file_server.ts"
33+
root: "<%= denoRoot %>/client"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { denoServerAdapter } from '@builder.io/qwik-city/adapters/deno-server/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.deno.ts', '@qwik-city-plan'],
11+
},
12+
minify: false,
13+
},
14+
plugins: [
15+
denoServerAdapter({
16+
ssg: {
17+
include: ['/*'],
18+
origin: 'https://<%= site %>',
19+
}
20+
}),
21+
],
22+
};
23+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* WHAT IS THIS FILE?
3+
*
4+
* It's the entry point for the Deno HTTP server when building for production.
5+
*
6+
* Learn more about the Deno integration here:
7+
* - https://qwik.builder.io/docs/deployments/deno/
8+
* - https://deno.com/manual/examples/http_server
9+
*
10+
*/
11+
import { createQwikCity } from '@builder.io/qwik-city/middleware/deno';
12+
import qwikCityPlan from '@qwik-city-plan';
13+
import { manifest } from '@qwik-client-manifest';
14+
import render from './entry.ssr';
15+
16+
// Create the Qwik City Deno middleware
17+
const { router, notFound, staticFile } = createQwikCity({
18+
render,
19+
qwikCityPlan,
20+
manifest,
21+
static: {
22+
root: '<%= denoRoot %>/client'
23+
}
24+
});
25+
26+
// Allow for dynamic port
27+
const port = Number(Deno.env.get('PORT') ?? 8080);
28+
29+
// Start the Deno server
30+
const server = Deno.listen({ port });
31+
32+
/* eslint-disable */
33+
console.log(`Server started: http://localhost:${port}/`);
34+
35+
// https://deno.com/manual/examples/http_server
36+
// Connections to the server will be yielded up as an async iterable.
37+
for await (const conn of server) {
38+
serveHttp(conn);
39+
}
40+
41+
async function serveHttp(conn: any) {
42+
const httpConn = Deno.serveHttp(conn);
43+
44+
// Each request sent over the HTTP connection will be yielded as an
45+
// async iterator from the HTTP connection.
46+
for await (const requestEvent of httpConn) {
47+
const staticResponse = await staticFile(requestEvent.request);
48+
if (staticResponse) {
49+
// Serve static file
50+
requestEvent.respondWith(staticResponse);
51+
continue;
52+
}
53+
54+
// Server-side render this request with Qwik City
55+
const qwikCityResponse = await router(requestEvent.request);
56+
if (qwikCityResponse) {
57+
requestEvent.respondWith(qwikCityResponse);
58+
continue;
59+
}
60+
61+
// Path not found
62+
requestEvent.respondWith(notFound(requestEvent.request));
63+
}
64+
}
65+
66+
declare const Deno: any;
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
2+
import {
3+
Tree,
4+
readProjectConfiguration,
5+
updateProjectConfiguration,
6+
} from '@nx/devkit';
7+
8+
import { denoIntegrationGenerator } from './generator';
9+
import applicationGenerator from './../../application/generator';
10+
import { DenoIntegrationGeneratorSchema } from './schema';
11+
import { Linter } from '@nx/linter';
12+
13+
describe('deno-integration generator', () => {
14+
let appTree: Tree;
15+
const projectName = 'test-project';
16+
const options: DenoIntegrationGeneratorSchema = {
17+
project: projectName,
18+
denoProjectName: 'cute-frog-420',
19+
};
20+
21+
beforeEach(async () => {
22+
appTree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
23+
24+
await 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 denoIntegrationGenerator(appTree, options);
37+
const config = readProjectConfiguration(appTree, projectName);
38+
39+
expect(config.targets!['build.ssr'].configurations!['production']).toEqual({
40+
configFile: `apps/${projectName}/adapters/deno/vite.config.ts`,
41+
});
42+
43+
expect(config.targets!['deploy']).toEqual({
44+
executor: 'nx:run-commands',
45+
options: {
46+
cwd: `dist/apps/${projectName}/client`,
47+
command: `deployctl deploy --project=${options.denoProjectName} https://deno.land/std/http/file_server.ts --dry-run`,
48+
},
49+
configurations: {
50+
preview: {
51+
command: `deployctl deploy --project=${options.denoProjectName} https://deno.land/std/http/file_server.ts`,
52+
},
53+
production: {
54+
command: `deployctl deploy --project=${options.denoProjectName} --prod https://deno.land/std/http/file_server.ts`,
55+
},
56+
},
57+
dependsOn: ['build-deno'],
58+
});
59+
60+
expect(config.targets!['preview-deno']).toEqual({
61+
executor: 'nx:run-commands',
62+
options: {
63+
command: `deno run --allow-net --allow-read --allow-env dist/apps/${projectName}/server/entry.deno.js`,
64+
},
65+
dependsOn: ['build-deno'],
66+
});
67+
68+
expect(config.targets!['build-deno']).toEqual({
69+
executor: 'nx:run-commands',
70+
options: {
71+
command: `npx nx run ${options.project}:build:production`,
72+
},
73+
});
74+
});
75+
76+
it('should use other target name if deploy target is already defined', async () => {
77+
const configBefore = readProjectConfiguration(appTree, projectName);
78+
configBefore.targets!['deploy'] = { executor: 'nx:noop' };
79+
updateProjectConfiguration(appTree, projectName, configBefore);
80+
81+
await denoIntegrationGenerator(appTree, options);
82+
83+
const config = readProjectConfiguration(appTree, projectName);
84+
expect(config.targets!['build.ssr'].configurations!['production']).toEqual({
85+
configFile: `apps/${projectName}/adapters/deno/vite.config.ts`,
86+
});
87+
expect(config.targets!['deploy']).toEqual({ executor: 'nx:noop' });
88+
expect(config.targets!['deploy.deno']).toEqual({
89+
executor: 'nx:run-commands',
90+
options: {
91+
cwd: `dist/apps/${projectName}/client`,
92+
command: `deployctl deploy --project=${options.denoProjectName} https://deno.land/std/http/file_server.ts --dry-run`,
93+
},
94+
configurations: {
95+
preview: {
96+
command: `deployctl deploy --project=${options.denoProjectName} https://deno.land/std/http/file_server.ts`,
97+
},
98+
production: {
99+
command: `deployctl deploy --project=${options.denoProjectName} --prod https://deno.land/std/http/file_server.ts`,
100+
},
101+
},
102+
dependsOn: ['build-deno'],
103+
});
104+
});
105+
106+
it('should use the name of the integration if configuration name "production" is already defined', async () => {
107+
const configBefore = readProjectConfiguration(appTree, projectName);
108+
configBefore.targets!['build.ssr'].configurations!['production'] = {};
109+
updateProjectConfiguration(appTree, projectName, configBefore);
110+
111+
await denoIntegrationGenerator(appTree, options);
112+
113+
const config = readProjectConfiguration(appTree, projectName);
114+
expect(
115+
config.targets!['build'].configurations!['production']
116+
).toBeUndefined();
117+
expect(config.targets!['build.ssr'].configurations!['production']).toEqual(
118+
{}
119+
);
120+
expect(config.targets!['build.ssr'].configurations!['deno']).toEqual({
121+
configFile: `apps/${projectName}/adapters/deno/vite.config.ts`,
122+
});
123+
124+
expect(config.targets!['deploy']).toEqual({
125+
executor: 'nx:run-commands',
126+
options: {
127+
cwd: `dist/apps/${projectName}/client`,
128+
command: `deployctl deploy --project=${options.denoProjectName} https://deno.land/std/http/file_server.ts --dry-run`,
129+
},
130+
configurations: {
131+
preview: {
132+
command: `deployctl deploy --project=${options.denoProjectName} https://deno.land/std/http/file_server.ts`,
133+
},
134+
production: {
135+
command: `deployctl deploy --project=${options.denoProjectName} --prod https://deno.land/std/http/file_server.ts`,
136+
},
137+
},
138+
dependsOn: ['build-deno'],
139+
});
140+
expect(config.targets!['build-deno']).toEqual({
141+
executor: 'nx:run-commands',
142+
options: {
143+
command: `npx nx run ${options.project}:build:deno`,
144+
},
145+
});
146+
});
147+
148+
it('should generate deploy.yml file for github actions', async () => {
149+
await denoIntegrationGenerator(appTree, {
150+
...options,
151+
generateGithubHook: true,
152+
});
153+
const hasDeployYaml = appTree.exists('.github/workflows/deploy.yml');
154+
expect(hasDeployYaml).toBeTruthy();
155+
});
156+
157+
describe('should throw if project configuration does not meet the expectations', () => {
158+
it('project is not an application', async () => {
159+
const config = readProjectConfiguration(appTree, projectName);
160+
config.projectType = 'library';
161+
updateProjectConfiguration(appTree, projectName, config);
162+
163+
await expect(denoIntegrationGenerator(appTree, options)).rejects.toThrow(
164+
'Cannot setup deno integration for the given project.'
165+
);
166+
});
167+
168+
it('project does not have Qwik\'s "build-ssr" target', async () => {
169+
const config = readProjectConfiguration(appTree, projectName);
170+
config.targets!.build.executor = 'nx:run-commands';
171+
updateProjectConfiguration(appTree, projectName, config);
172+
173+
await expect(denoIntegrationGenerator(appTree, options)).rejects.toThrow(
174+
'Project contains invalid configuration.'
175+
);
176+
});
177+
});
178+
});

0 commit comments

Comments
 (0)