Skip to content

Commit 61b46ec

Browse files
feat(qwik-nx): netlify integration (#111)
1 parent 12ae512 commit 61b46ec

File tree

20 files changed

+606
-10
lines changed

20 files changed

+606
-10
lines changed

e2e/qwik-nx-e2e/tests/qwik-nx-cloudflare.spec.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ describe('qwik nx cloudflare generator', () => {
4343
await runNxCommandAsync(
4444
`generate qwik-nx:cloudflare-pages-integration ${project} --no-interactive`
4545
);
46-
47-
// move header component into the library
4846
}, DEFAULT_E2E_TIMEOUT);
4947

5048
it(
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {
2+
checkFilesExist,
3+
ensureNxProject,
4+
runNxCommandAsync,
5+
uniq,
6+
} from '@nx/plugin/testing';
7+
8+
import {
9+
runCommandUntil,
10+
promisifiedTreeKill,
11+
killPort,
12+
killPorts,
13+
DEFAULT_E2E_TIMEOUT,
14+
} from '@qwikifiers/e2e/utils';
15+
16+
const NETLIFY_PREVIEW_PORT = 8888;
17+
18+
describe('qwik nx netlify generator', () => {
19+
beforeAll(async () => {
20+
await killPorts(NETLIFY_PREVIEW_PORT);
21+
ensureNxProject('qwik-nx', 'dist/packages/qwik-nx');
22+
}, 10000);
23+
24+
afterAll(async () => {
25+
await runNxCommandAsync('reset');
26+
});
27+
28+
describe('should build and serve a project with the netlify adapter', () => {
29+
let project: string;
30+
beforeAll(async () => {
31+
project = uniq('qwik-nx');
32+
await runNxCommandAsync(
33+
`generate qwik-nx:app ${project} --no-interactive`
34+
);
35+
await runNxCommandAsync(
36+
`generate qwik-nx:netlify-integration ${project} --no-interactive`
37+
);
38+
}, DEFAULT_E2E_TIMEOUT);
39+
40+
it(
41+
'should be able to successfully build the application',
42+
async () => {
43+
const result = await runNxCommandAsync(`build-netlify ${project}`);
44+
expect(result.stdout).toContain(
45+
`Successfully ran target build for project ${project}`
46+
);
47+
expect(() =>
48+
checkFilesExist(`dist/apps/${project}/client/q-manifest.json`)
49+
).not.toThrow();
50+
expect(() =>
51+
checkFilesExist(
52+
`dist/apps/${project}/.netlify/edge-functions/entry.netlify-edge/entry.netlify-edge.js`
53+
)
54+
).not.toThrow();
55+
},
56+
DEFAULT_E2E_TIMEOUT
57+
);
58+
59+
it(
60+
'should serve application in preview mode with custom port',
61+
async () => {
62+
const p = await runCommandUntil(
63+
`run ${project}:preview-netlify`,
64+
(output) => {
65+
console.log(output);
66+
return output.includes('Server now ready on http://localhost:8888');
67+
}
68+
);
69+
try {
70+
await promisifiedTreeKill(p.pid!, 'SIGKILL');
71+
await killPort(NETLIFY_PREVIEW_PORT);
72+
} catch {
73+
// ignore
74+
}
75+
},
76+
DEFAULT_E2E_TIMEOUT
77+
);
78+
});
79+
});

packages/qwik-nx/executors.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
"implementation": "./src/executors/micro-frontends-preview-server/executor",
1616
"schema": "./src/executors/micro-frontends-preview-server/schema.json",
1717
"description": "Serve a host application along with its known remotes in a preview mode."
18+
},
19+
"exec": {
20+
"implementation": "./src/executors/exec/executor",
21+
"schema": "./src/executors/exec/schema.json",
22+
"description": "Run a single command using child_process. It is a simplified version of \"nx:run-commands\" executor that allows running interactive commands."
1823
}
1924
}
2025
}

packages/qwik-nx/generators.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,12 @@
5353
"cloudflare-pages-integration": {
5454
"factory": "./src/generators/integrations/cloudflare-pages-integration/generator",
5555
"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"
56+
"description": "Qwik City Cloudflare Pages adapter allows you to connect Qwik City to Cloudflare Pages"
57+
},
58+
"netlify-integration": {
59+
"factory": "./src/generators/integrations/netlify/generator",
60+
"schema": "./src/generators/integrations/netlify/schema.json",
61+
"description": "Qwik City Netlify Edge adapter allows you to connect Qwik City to Netlify Edge Functions."
5762
},
5863
"host": {
5964
"factory": "./src/generators/host/generator",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { execSync } from 'node:child_process';
2+
import { ExecExecutorSchema } from './schema';
3+
import { ExecutorContext } from '@nx/devkit';
4+
import { isAbsolute, join } from 'path';
5+
6+
const LARGE_BUFFER = 1024 * 1000000;
7+
8+
function calculateCwd(
9+
cwd: string | undefined,
10+
context: ExecutorContext
11+
): string {
12+
if (!cwd) return context.root;
13+
if (isAbsolute(cwd)) return cwd;
14+
return join(context.root, cwd);
15+
}
16+
17+
export default async function runExecutor(
18+
options: ExecExecutorSchema,
19+
context: ExecutorContext
20+
) {
21+
let success = false;
22+
try {
23+
execSync(options.command, {
24+
stdio: [0, 1, 2],
25+
cwd: calculateCwd(options.cwd, context),
26+
maxBuffer: LARGE_BUFFER,
27+
});
28+
success = true;
29+
} catch {
30+
success = false;
31+
}
32+
return {
33+
success,
34+
};
35+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface ExecExecutorSchema {
2+
command: string;
3+
cwd?: string;
4+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"$schema": "http://json-schema.org/schema",
3+
"version": 2,
4+
"title": "Run a single command",
5+
"description": "Run a single command using child_process. It is a simplified version of \"nx:run-commands\" executor that allows running interactive commands.",
6+
"type": "object",
7+
"outputCapture": "direct-nodejs",
8+
"properties": {
9+
"command": {
10+
"type": "string",
11+
"description": "Command to run in child process.",
12+
"x-priority": "important"
13+
},
14+
"cwd": {
15+
"type": "string",
16+
"description": "Current working directory of the commands. If it's not specified the commands will run in the workspace root, if a relative path is specified the commands will run in that path relative to the workspace root and if it's an absolute path the commands will run in that path."
17+
}
18+
},
19+
"required": ["command"]
20+
}

packages/qwik-nx/src/generators/integrations/cloudflare-pages-integration/generator.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '../../../utils/integration-configuration-name';
1919
import { nxCloudflareWrangler, wranglerVersion } from '../../../utils/versions';
2020
import { CloudflarePagesIntegrationGeneratorSchema } from './schema';
21+
import { isQwikNxProject } from '../../../utils/migrations';
2122

2223
interface NormalizedOptions {
2324
offsetFromRoot: string;
@@ -34,11 +35,8 @@ export async function cloudflarePagesIntegrationGenerator(
3435
'Cannot setup cloudflare integration for the given project.'
3536
);
3637
}
37-
if (config.targets?.['build']?.executor !== 'qwik-nx:build') {
38-
throw new Error(
39-
'Project contains invalid configuration. ' +
40-
'If you encounter this error within a Qwik project, make sure you have run necessary Nx migrations for qwik-nx plugin.'
41-
);
38+
if (!isQwikNxProject(config)) {
39+
throw new Error('Project contains invalid configuration.');
4240
}
4341

4442
const configurationName = getIntegrationConfigurationName(
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`netlify-integration generator should add required targets 1`] = `
4+
Array [
5+
Object {
6+
"path": "apps/test-project/adapters/netlify/vite.config.ts",
7+
"type": "CREATE",
8+
},
9+
Object {
10+
"path": "apps/test-project/public/_headers",
11+
"type": "CREATE",
12+
},
13+
Object {
14+
"path": "apps/test-project/src/entry.netlify-edge.tsx",
15+
"type": "CREATE",
16+
},
17+
]
18+
`;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { netlifyEdgeAdapter } from '@builder.io/qwik-city/adapters/netlify-edge/vite';
2+
import { extendConfig } from '@builder.io/qwik-city/vite';
3+
import { qwikVite } from '@builder.io/qwik/optimizer';
4+
import { UserConfig, Plugin } from 'vite';
5+
import { join } from 'path';
6+
import baseConfig from '../../vite.config';
7+
8+
const modified: UserConfig = {
9+
...baseConfig,
10+
// vite does not override plugins in it's "mergeConfig" util
11+
plugins: (baseConfig as UserConfig).plugins?.filter(
12+
(p) => (p as Plugin)?.name !== 'vite-plugin-qwik'
13+
),
14+
};
15+
export default extendConfig(modified, () => {
16+
const outDir = 'dist/<%= projectRoot %>';
17+
const ssrOutDir = join(outDir, '.netlify/edge-functions/entry.netlify-edge');
18+
19+
return {
20+
build: {
21+
ssr: true,
22+
rollupOptions: {
23+
input: ['<%= projectRoot %>/src/entry.netlify-edge.tsx', '@qwik-city-plan'],
24+
},
25+
outDir: ssrOutDir,
26+
},
27+
plugins: [
28+
netlifyEdgeAdapter(),
29+
qwikVite({
30+
client: {
31+
outDir: join('<%= offsetFromRoot %>', outDir, 'client'),
32+
},
33+
ssr: {
34+
outDir: join('<%= offsetFromRoot %>', ssrOutDir),
35+
},
36+
}),
37+
],
38+
};
39+
});

0 commit comments

Comments
 (0)