vyriy 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@ import { exec as processExec } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
3
  import packageJson from '../package.json' with { type: 'json' };
4
4
  const exec = promisify(processExec);
5
+ const yarnStableHint = 'Try:\n corepack enable\n corepack prepare yarn@stable --activate';
5
6
  const node = () => {
6
7
  const majorVersion = Number.parseInt(process.versions.node.split('.')[0]);
7
8
  const minimumMajorVersion = Number.parseInt(packageJson.engines.node.match(/(\d+)/)?.[0]);
@@ -18,6 +19,39 @@ const node = () => {
18
19
  message: `Vyriy requires Node.js >= ${minimumMajorVersion}.\n\nCurrent version: ${process.versions.node}\n\nPlease upgrade Node.js and run the command again.`,
19
20
  };
20
21
  };
22
+ const corepack = async () => {
23
+ let currentVersion;
24
+ try {
25
+ const { stdout } = await exec('corepack --version');
26
+ currentVersion = stdout.trim();
27
+ }
28
+ catch {
29
+ return {
30
+ ok: false,
31
+ message: `Corepack was not found.\n\nVyriy uses Corepack to install Yarn stable.\n\nInstall a Node.js distribution that includes Corepack and run the command again.`,
32
+ };
33
+ }
34
+ return {
35
+ ok: true,
36
+ message: `Corepack ${currentVersion}`,
37
+ };
38
+ };
39
+ const activateYarnStable = async () => {
40
+ try {
41
+ await exec('corepack enable');
42
+ await exec('corepack prepare yarn@stable --activate');
43
+ }
44
+ catch {
45
+ return {
46
+ ok: false,
47
+ message: `Corepack could not activate Yarn stable.\n\n${yarnStableHint}`,
48
+ };
49
+ }
50
+ return {
51
+ ok: true,
52
+ message: 'Yarn stable activated',
53
+ };
54
+ };
21
55
  const yarn = async () => {
22
56
  const minimumMajorVersion = Number.parseInt(packageJson.packageManager.match(/(\d+)/)?.[0]);
23
57
  let currentVersion;
@@ -28,7 +62,7 @@ const yarn = async () => {
28
62
  catch {
29
63
  return {
30
64
  ok: false,
31
- message: `Yarn was not found.\n\nVyriy requires Yarn >= ${minimumMajorVersion}.\n\nTry:\n corepack enable\n yarn set version stable`,
65
+ message: `Yarn was not found.\n\nVyriy requires Yarn >= ${minimumMajorVersion}.\n\n${yarnStableHint}`,
32
66
  };
33
67
  }
34
68
  const majorVersion = Number.parseInt(currentVersion.match(/(\d+)/)?.[0]);
@@ -40,7 +74,7 @@ const yarn = async () => {
40
74
  }
41
75
  return {
42
76
  ok: false,
43
- message: `Vyriy requires Yarn >= ${minimumMajorVersion}.\n\nCurrent version: ${currentVersion}\n\nTry:\n corepack enable\n yarn set version stable`,
77
+ message: `Vyriy requires Yarn >= ${minimumMajorVersion}.\n\nCurrent version: ${currentVersion}\n\n${yarnStableHint}`,
44
78
  };
45
79
  };
46
80
  export const checkEnv = async () => {
@@ -53,7 +87,28 @@ export const checkEnv = async () => {
53
87
  console.error(nodeResults.message);
54
88
  return 1;
55
89
  }
56
- const yarnResults = await yarn();
90
+ const corepackResults = await corepack();
91
+ if (corepackResults.ok) {
92
+ console.log(' ', corepackResults.message);
93
+ }
94
+ else {
95
+ console.error(corepackResults.message);
96
+ return 1;
97
+ }
98
+ let yarnResults = await yarn();
99
+ if (yarnResults.ok) {
100
+ console.log(' ', yarnResults.message);
101
+ return 0;
102
+ }
103
+ const yarnStableResults = await activateYarnStable();
104
+ if (yarnStableResults.ok) {
105
+ console.log(' ', yarnStableResults.message);
106
+ }
107
+ else {
108
+ console.error(yarnStableResults.message);
109
+ return 1;
110
+ }
111
+ yarnResults = await yarn();
57
112
  if (yarnResults.ok) {
58
113
  console.log(' ', yarnResults.message);
59
114
  }
@@ -2,7 +2,7 @@ export declare const plan: (dirName: string, appPath: string) => Promise<{
2
2
  name: string;
3
3
  description: string;
4
4
  target: string;
5
- preset: "ssr" | "base" | "rest" | "api" | "library" | "ssg" | "spa";
5
+ preset: "ssr" | "base" | "rest" | "api" | "library" | "gql" | "ssg" | "spa";
6
6
  scope: string | undefined;
7
7
  ci: import("../preset/types.js").CiProvider | undefined;
8
8
  deploy: import("../preset/types.js").DeployProvider | undefined;
@@ -48,7 +48,6 @@ export const api = {
48
48
  '@vyriy/storybook-config': `^${packageJson.version}`,
49
49
  storybook: packageJson.peerDependencies.storybook,
50
50
  '@vyriy/path': `^${packageJson.version}`,
51
- vyriy: `^${packageJson.version}`,
52
51
  husky: packageJson.peerDependencies.husky,
53
52
  'npm-run-all2': packageJson.peerDependencies['npm-run-all2'],
54
53
  'cross-env': packageJson.peerDependencies['cross-env'],
@@ -57,7 +56,6 @@ export const api = {
57
56
  '@vyriy/handler': `^${packageJson.version}`,
58
57
  '@vyriy/server': `^${packageJson.version}`,
59
58
  tsx: packageJson.peerDependencies.tsx,
60
- webpack: packageJson.peerDependencies.webpack,
61
59
  'webpack-cli': packageJson.peerDependencies['webpack-cli'],
62
60
  },
63
61
  }, null, 2) + '\n',
@@ -1,16 +1,750 @@
1
+ import packageJson from '../../../package.json' with { type: 'json' };
1
2
  import { base } from './base.js';
2
3
  export const gql = {
3
4
  files: (options) => ({
4
5
  ...base.files(options),
6
+ 'package.json': JSON.stringify({
7
+ name: options.name,
8
+ version: '0.0.0',
9
+ description: options.description,
10
+ private: true,
11
+ type: 'module',
12
+ agents: './AGENTS.md',
13
+ packageManager: packageJson.packageManager,
14
+ engines: {
15
+ node: packageJson.engines.node,
16
+ },
17
+ workspaces: [
18
+ 'packages/*',
19
+ 'workspaces/*',
20
+ ],
21
+ scripts: {
22
+ storybook: 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook dev -p 6006 --disable-telemetry',
23
+ check: 'run-s lint build test',
24
+ fix: "run-s 'fix:*'",
25
+ start: "run-p 'start:*'",
26
+ lint: "run-s 'lint:*'",
27
+ build: "run-s 'build:*'",
28
+ test: "run-s 'test:*'",
29
+ 'fix:prettier': 'prettier . --write',
30
+ 'fix:eslint': 'eslint . --fix',
31
+ 'start:graphql': 'sh workspaces/graphql/bin/start.sh',
32
+ 'lint:ts': 'tsc',
33
+ 'lint:prettier': 'prettier . --check',
34
+ 'lint:eslint': 'eslint .',
35
+ 'build:graphql': 'rimraf dist && sh workspaces/graphql/bin/build.sh',
36
+ 'build:storybook': 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook build --quiet --disable-telemetry',
37
+ 'test:jest': 'jest',
38
+ postinstall: 'husky',
39
+ },
40
+ dependencies: {
41
+ '@vyriy/typescript-config': `^${packageJson.version}`,
42
+ typescript: packageJson.peerDependencies.typescript,
43
+ '@vyriy/prettier-config': `^${packageJson.version}`,
44
+ prettier: packageJson.peerDependencies.prettier,
45
+ '@vyriy/eslint-config': `^${packageJson.version}`,
46
+ eslint: packageJson.peerDependencies.eslint,
47
+ '@vyriy/jest-config': `^${packageJson.version}`,
48
+ jest: packageJson.peerDependencies.jest,
49
+ '@vyriy/storybook-config': `^${packageJson.version}`,
50
+ storybook: packageJson.peerDependencies.storybook,
51
+ '@vyriy/path': `^${packageJson.version}`,
52
+ husky: packageJson.peerDependencies.husky,
53
+ 'npm-run-all2': packageJson.peerDependencies['npm-run-all2'],
54
+ 'cross-env': packageJson.peerDependencies['cross-env'],
55
+ rimraf: packageJson.peerDependencies.rimraf,
56
+ '@vyriy/webpack-config': `^${packageJson.version}`,
57
+ '@vyriy/handler': `^${packageJson.version}`,
58
+ '@vyriy/server': `^${packageJson.version}`,
59
+ '@vyriy/router': `^${packageJson.version}`,
60
+ tsx: packageJson.peerDependencies.tsx,
61
+ 'webpack-cli': packageJson.peerDependencies['webpack-cli'],
62
+ graphql: packageJson.peerDependencies.graphql,
63
+ '@vyriy/html': `^${packageJson.version}`,
64
+ },
65
+ }, null, 2) + '\n',
66
+ 'packages/graphql/doc.mdx': `import { Meta, Markdown } from '@storybook/addon-docs/blocks';
67
+ import ReadMe from './README.md?raw';
68
+
69
+ <Meta title="Packages/GraphQL" />
70
+
71
+ <Markdown>{ReadMe}</Markdown>
72
+ `,
73
+ 'packages/graphql/graphiql.test.ts': `import { describe, expect, it } from '@jest/globals';
74
+
75
+ import { graphiql } from './graphiql.js';
76
+
77
+ describe('packages/graphql/graphiql.ts', () => {
78
+ it('returns the embedded GraphiQL HTML page', () => {
79
+ const page = graphiql();
80
+
81
+ expect(page).toContain('<html lang="en">');
82
+ expect(page).toContain('<title>GraphiQL</title>');
83
+ expect(page).toContain('<meta charset="UTF-8"');
84
+ expect(page).toContain('<div id="graphiql"><div class="loading">Loading');
85
+ expect(page).toContain('https://esm.sh/graphiql@5.2.2/dist/style.css');
86
+ expect(page).toContain('https://esm.sh/@graphiql/plugin-explorer@5.1.1/dist/style.css');
87
+ expect(page).toContain('<script type="importmap">');
88
+ expect(page).toContain('"graphiql": "https://esm.sh/graphiql@5.2.2?standalone');
89
+ expect(page).toContain('const fetcher = createGraphiQLFetcher({');
90
+ expect(page).toContain("url: 'http://localhost:3000'");
91
+ expect(page).toContain('root.render(React.createElement(App));');
92
+ });
93
+ });
94
+ `,
95
+ 'packages/graphql/graphiql.ts': `import { html, minify } from '@vyriy/html';
96
+
97
+ export const graphiql = () =>
98
+ minify(
99
+ html({
100
+ htmlAttributes: 'lang="en"',
101
+ title: '<title>GraphiQL</title>',
102
+ meta: '<meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" />',
103
+ link: [
104
+ \`<link
105
+ rel="stylesheet"
106
+ href="https://esm.sh/graphiql@5.2.2/dist/style.css"
107
+ integrity="sha384-f6GHLfCwoa4MFYUMd3rieGOsIVAte/evKbJhMigNdzUf52U9bV2JQBMQLke0ua+2"
108
+ crossorigin="anonymous"
109
+ />\`,
110
+ \`<link
111
+ rel="stylesheet"
112
+ href="https://esm.sh/@graphiql/plugin-explorer@5.1.1/dist/style.css"
113
+ integrity="sha384-vTFGj0krVqwFXLB7kq/VHR0/j2+cCT/B63rge2mULaqnib2OX7DVLUVksTlqvMab"
114
+ crossorigin="anonymous"
115
+ />\`,
116
+ ].join(''),
117
+ style: \`<style>
118
+ body {
119
+ margin: 0;
120
+ }
121
+
122
+ #graphiql {
123
+ height: 100dvh;
124
+ }
125
+
126
+ .loading {
127
+ height: 100%;
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ font-size: 4rem;
132
+ }
133
+ </style>\`,
134
+ script: [
135
+ \`<script type="importmap">
136
+ {
137
+ "imports": {
138
+ "react": "https://esm.sh/react@19.2.5",
139
+ "react/": "https://esm.sh/react@19.2.5/",
140
+ "react-dom": "https://esm.sh/react-dom@19.2.5",
141
+ "react-dom/": "https://esm.sh/react-dom@19.2.5/",
142
+ "graphiql": "https://esm.sh/graphiql@5.2.2?standalone&external=react,react-dom,@graphiql/react,graphql",
143
+ "graphiql/": "https://esm.sh/graphiql@5.2.2/",
144
+ "@graphiql/plugin-explorer": "https://esm.sh/@graphiql/plugin-explorer@5.1.1?standalone&external=react,@graphiql/react,graphql",
145
+ "@graphiql/react": "https://esm.sh/@graphiql/react@0.37.3?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid",
146
+ "@graphiql/toolkit": "https://esm.sh/@graphiql/toolkit@0.11.3?standalone&external=graphql",
147
+ "graphql": "https://esm.sh/graphql@16.13.2",
148
+ "@emotion/is-prop-valid": "data:text/javascript,"
149
+ },
150
+ "integrity": {
151
+ "https://esm.sh/react@19.2.5": "sha384-ZNmUQ9QQgyl95nnD/FJTBQn2ZEPTbWtMuWCXTKWNuF6Si7nC+6bvSgk5LWu+ELHn",
152
+ "https://esm.sh/react-dom@19.2.5": "sha384-qtNxBzn9gBs3CmJItMuvIVyjW3VIU0/rzGhCm9MippVU1BpR/c4VgaFYDIg/FrY2",
153
+ "https://esm.sh/graphiql@5.2.2": "sha384-MBVZMq1pmz8DwpwIWPWLk2tmS6tGiSi6WwbXvy9NhuDYASAAWd2m96xbxLqszig9",
154
+ "https://esm.sh/graphiql@5.2.2?standalone&external=react,react-dom,@graphiql/react,graphql": "sha384-SzHBEbcQfhvmwqh5Vtat9k7b/kIzmdVO3KMzQiAYwcxCA9x7vZwFRUgjzN1AeV3q",
155
+ "https://esm.sh/@graphiql/plugin-explorer@5.1.1": "sha384-83REbLb9KtIhL/6J1n91SLoP0648KOKZLIDdHRx/a0E7T3ajq6PzKz+815SCfN52",
156
+ "https://esm.sh/@graphiql/react@0.37.3?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid": "sha384-iZsbTy9B0VcX2BOTdqMuX0uJ9Hff5GbG2QeOt4OeMp0GHza76dwQaYQYNYkZkIVq",
157
+ "https://esm.sh/@graphiql/toolkit@0.11.3?standalone&external=graphql": "sha384-ZsnupyYmzpNjF1Z/81zwi4nV352n4P7vm0JOFKiYnAwVGOf9twnEMnnxmxabMBXe",
158
+ "https://esm.sh/graphql@16.13.2": "sha384-TQg9alwG3P9fzBErDW011vKuyTnrwpBZsl3SdMAh6DwBcv9ezFOl0djGI/68VOyy"
159
+ }
160
+ }
161
+ </script>\`,
162
+ \`<script type="module">
163
+ import React from 'react';
164
+ import ReactDOM from 'react-dom/client';
165
+ import { GraphiQL, HISTORY_PLUGIN } from 'graphiql';
166
+ import { createGraphiQLFetcher } from '@graphiql/toolkit';
167
+ import { explorerPlugin } from '@graphiql/plugin-explorer';
168
+ import 'graphiql/setup-workers/esm.sh';
169
+
170
+ const fetcher = createGraphiQLFetcher({
171
+ url: 'http://localhost:3000',
172
+ });
173
+ const plugins = [HISTORY_PLUGIN, explorerPlugin()];
174
+
175
+ function App() {
176
+ return React.createElement(GraphiQL, {
177
+ fetcher,
178
+ plugins,
179
+ defaultEditorToolsVisibility: true,
180
+ });
181
+ }
182
+
183
+ const container = document.getElementById('graphiql');
184
+ const root = ReactDOM.createRoot(container);
185
+ root.render(React.createElement(App));
186
+ </script>\`,
187
+ ].join(''),
188
+ body: '<div id="graphiql"><div class="loading">Loading…</div></div>',
5
189
  }),
6
- ci: {
7
- github: {
8
- '.gitlab-ci.yml': 'code',
190
+ );
191
+ `,
192
+ 'packages/graphql/index.test.ts': `import { describe, expect, it } from '@jest/globals';
193
+
194
+ import { router } from './router.js';
195
+
196
+ describe('packages/graphql/index.ts', () => {
197
+ it('re-exports the GraphQL router', async () => {
198
+ await expect(import('./index.js')).resolves.toMatchObject({
199
+ router,
200
+ });
201
+ });
202
+ });
203
+ `,
204
+ 'packages/graphql/index.ts': "export * from './router.js';\n",
205
+ 'packages/graphql/package.json': `{
206
+ "name": "@p/graphql",
207
+ "type": "module",
208
+ "private": true
209
+ }
210
+ `,
211
+ 'packages/graphql/README.md': `# @p/graphql
212
+
213
+ Reusable GraphQL API package for the application.
214
+
215
+ ## Exports
216
+
217
+ - \`router\` - an \`@vyriy/router\` instance that serves the GraphiQL page and GraphQL HTTP requests.
218
+
219
+ ## Routes
220
+
221
+ ### \`GET /\`
222
+
223
+ Returns an embedded GraphiQL page for exploring the schema in a browser.
224
+
225
+ ### \`POST /\`
226
+
227
+ The router expects a JSON body with a GraphQL request:
228
+
229
+ \`\`\`json
230
+ {
231
+ "query": "{ hello }",
232
+ "variables": {},
233
+ "operationName": "Hello"
234
+ }
235
+ \`\`\`
236
+
237
+ Invalid JSON or an empty body returns \`400\` with \`Invalid JSON body\`. A body without \`query\` returns \`400\` with \`Missing GraphQL query\`.
238
+
239
+ ## Schema
240
+
241
+ The package schema currently supports:
242
+
243
+ - \`hello: String\`
244
+ - \`test: Test\`
245
+ - \`ping(message: String): String\`
246
+
247
+ Example mutation:
248
+
249
+ \`\`\`graphql
250
+ mutation Ping($message: String) {
251
+ ping(message: $message)
252
+ }
253
+ \`\`\`
254
+ `,
255
+ 'packages/graphql/router.test.ts': `import { describe, expect, it } from '@jest/globals';
256
+ import type { APIGatewayProxyEvent, APIGatewayProxyResult } from '@vyriy/router';
257
+
258
+ import { router } from './router.js';
259
+
260
+ type GraphQLBody = {
261
+ data?: Record<string, unknown> | null;
262
+ errors?: Array<{
263
+ message: string;
264
+ }>;
265
+ };
266
+
267
+ const getPostEvent = (
268
+ body: string | null,
269
+ headers: APIGatewayProxyEvent['headers'] | null = {
270
+ authorization: 'Bearer token',
271
+ },
272
+ ): APIGatewayProxyEvent =>
273
+ ({
274
+ body,
275
+ headers,
276
+ httpMethod: 'POST',
277
+ path: '/',
278
+ pathParameters: null,
279
+ queryStringParameters: null,
280
+ }) as unknown as APIGatewayProxyEvent;
281
+
282
+ const getEvent = (path = '/'): APIGatewayProxyEvent =>
283
+ ({
284
+ body: null,
285
+ headers: {},
286
+ httpMethod: 'GET',
287
+ path,
288
+ pathParameters: null,
289
+ queryStringParameters: null,
290
+ }) as unknown as APIGatewayProxyEvent;
291
+
292
+ const parseBody = <Value>(response: APIGatewayProxyResult): Value => JSON.parse(response.body) as Value;
293
+
294
+ describe('packages/graphql/router.ts', () => {
295
+ it('serves the embedded GraphiQL page', async () => {
296
+ const response = await router.route(getEvent());
297
+
298
+ expect(response.statusCode).toBe(200);
299
+ expect(response.headers).toEqual({
300
+ 'Content-Type': 'text/html; charset=UTF-8',
301
+ });
302
+ expect(response.body).toContain('<title>GraphiQL</title>');
303
+ expect(response.body).toContain('<div id="graphiql">');
304
+ });
305
+
306
+ it('executes GraphQL requests', async () => {
307
+ const response = await router.route(
308
+ getPostEvent(
309
+ JSON.stringify({
310
+ query: 'query Hello { hello }',
311
+ operationName: 'Hello',
312
+ }),
313
+ ),
314
+ );
315
+
316
+ expect(response.statusCode).toBe(200);
317
+ expect(parseBody<GraphQLBody>(response)).toEqual({
318
+ data: {
319
+ hello: 'Hello from GraphQL',
320
+ },
321
+ });
322
+ });
323
+
324
+ it('passes variables to GraphQL execution', async () => {
325
+ const response = await router.route(
326
+ getPostEvent(
327
+ JSON.stringify({
328
+ query: 'mutation Ping($message: String) { ping(message: $message) }',
329
+ variables: {
330
+ message: 'from router',
331
+ },
332
+ }),
333
+ ),
334
+ );
335
+
336
+ expect(response.statusCode).toBe(200);
337
+ expect(parseBody<GraphQLBody>(response)).toEqual({
338
+ data: {
339
+ ping: 'pong: from router',
340
+ },
341
+ });
342
+ });
343
+
344
+ it('executes GraphQL requests without request headers', async () => {
345
+ const response = await router.route(
346
+ getPostEvent(
347
+ JSON.stringify({
348
+ query: '{ test { ok message } }',
349
+ }),
350
+ null,
351
+ ),
352
+ );
353
+
354
+ expect(response.statusCode).toBe(200);
355
+ expect(parseBody<GraphQLBody>(response)).toEqual({
356
+ data: {
357
+ test: {
358
+ ok: true,
359
+ message: 'GraphQL works',
360
+ },
361
+ },
362
+ });
363
+ });
364
+
365
+ it('rejects missing bodies', async () => {
366
+ await expect(router.route(getPostEvent(null))).resolves.toEqual({
367
+ body: JSON.stringify({
368
+ errors: [{ message: 'Invalid JSON body' }],
369
+ }),
370
+ statusCode: 400,
371
+ });
372
+ });
373
+
374
+ it('rejects invalid JSON bodies', async () => {
375
+ await expect(router.route(getPostEvent('{'))).resolves.toEqual({
376
+ body: JSON.stringify({
377
+ errors: [{ message: 'Invalid JSON body' }],
378
+ }),
379
+ statusCode: 400,
380
+ });
381
+ });
382
+
383
+ it('rejects bodies without a GraphQL query', async () => {
384
+ await expect(router.route(getPostEvent(JSON.stringify({ variables: {} })))).resolves.toEqual({
385
+ body: JSON.stringify({
386
+ errors: [{ message: 'Missing GraphQL query' }],
387
+ }),
388
+ statusCode: 400,
389
+ });
390
+ });
391
+ });
392
+ `,
393
+ 'packages/graphql/router.ts': `import { createRouter } from '@vyriy/router';
394
+
395
+ import { graphql } from 'graphql';
396
+
397
+ import { graphiql } from './graphiql.js';
398
+ import { schema } from './schema.js';
399
+
400
+ export const router = createRouter();
401
+
402
+ router.get('/', () => ({
403
+ headers: {
404
+ 'Content-Type': 'text/html; charset=UTF-8',
405
+ },
406
+ body: graphiql(),
407
+ }));
408
+
409
+ router.post('/', async (params) => {
410
+ let payload: {
411
+ query?: string;
412
+ variables?: Record<string, unknown>;
413
+ operationName?: string;
414
+ };
415
+
416
+ if (!params.body) {
417
+ return {
418
+ statusCode: 400,
419
+ body: JSON.stringify({
420
+ errors: [{ message: 'Invalid JSON body' }],
421
+ }),
422
+ };
423
+ }
424
+
425
+ try {
426
+ payload = JSON.parse(params.body) as typeof payload;
427
+ } catch {
428
+ return {
429
+ statusCode: 400,
430
+ body: JSON.stringify({
431
+ errors: [{ message: 'Invalid JSON body' }],
432
+ }),
433
+ };
434
+ }
435
+
436
+ if (!payload.query) {
437
+ return {
438
+ statusCode: 400,
439
+ body: JSON.stringify({
440
+ errors: [{ message: 'Missing GraphQL query' }],
441
+ }),
442
+ };
443
+ }
444
+
445
+ return {
446
+ body: JSON.stringify(
447
+ await graphql({
448
+ schema,
449
+ source: payload.query,
450
+ variableValues: payload.variables,
451
+ operationName: payload.operationName,
452
+ contextValue: {
453
+ event: params.event,
454
+ headers: params.headers ?? {},
455
+ },
456
+ }),
457
+ ),
458
+ };
459
+ });
460
+ `,
461
+ 'packages/graphql/schema.test.ts': `import { describe, expect, it } from '@jest/globals';
462
+ import { graphql } from 'graphql';
463
+
464
+ import { schema } from './schema.js';
465
+
466
+ describe('packages/graphql/schema.ts', () => {
467
+ it('resolves query fields', async () => {
468
+ await expect(
469
+ graphql({
470
+ schema,
471
+ source: '{ hello test { ok message } }',
472
+ }),
473
+ ).resolves.toEqual({
474
+ data: {
475
+ hello: 'Hello from GraphQL',
476
+ test: {
477
+ ok: true,
478
+ message: 'GraphQL works',
479
+ },
480
+ },
481
+ });
482
+ });
483
+
484
+ it('resolves the ping mutation', async () => {
485
+ await expect(
486
+ graphql({
487
+ schema,
488
+ source: 'mutation Ping($message: String) { ping(message: $message) }',
489
+ variableValues: {
490
+ message: 'hello',
491
+ },
492
+ }),
493
+ ).resolves.toEqual({
494
+ data: {
495
+ ping: 'pong: hello',
496
+ },
497
+ });
498
+ });
499
+ });
500
+ `,
501
+ 'packages/graphql/schema.ts': `import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLBoolean } from 'graphql';
502
+
503
+ const TestType = new GraphQLObjectType({
504
+ name: 'Test',
505
+ fields: {
506
+ ok: {
507
+ type: GraphQLBoolean,
508
+ },
509
+ message: {
510
+ type: GraphQLString,
511
+ },
512
+ },
513
+ });
514
+
515
+ const QueryType = new GraphQLObjectType({
516
+ name: 'Query',
517
+ fields: {
518
+ hello: {
519
+ type: GraphQLString,
520
+ resolve: () => 'Hello from GraphQL',
521
+ },
522
+
523
+ test: {
524
+ type: TestType,
525
+ resolve: () => ({
526
+ ok: true,
527
+ message: 'GraphQL works',
528
+ }),
529
+ },
530
+ },
531
+ });
532
+
533
+ const MutationType = new GraphQLObjectType({
534
+ name: 'Mutation',
535
+ fields: {
536
+ ping: {
537
+ type: GraphQLString,
538
+ args: {
539
+ message: {
540
+ type: GraphQLString,
9
541
  },
542
+ },
543
+ resolve: (_source, args: { message: string }) => {
544
+ return \`pong: \${args.message}\`;
545
+ },
10
546
  },
11
- deploy: {
12
- docker: {
13
- Dockerfile: 'code',
547
+ },
548
+ });
549
+
550
+ export const schema = new GraphQLSchema({
551
+ query: QueryType,
552
+ mutation: MutationType,
553
+ });
554
+ `,
555
+ 'workspaces/graphql/bin/build.sh': `#!/usr/bin/env sh
556
+
557
+ set -e
558
+
559
+ scriptdir="$PWD/workspaces/graphql";
560
+ distdir="dist/graphql";
561
+
562
+ NODE_ENV=production npx webpack --config $scriptdir/webpack.config.ts
563
+
564
+ cp $scriptdir/package.json $distdir/package.json
565
+ npm pkg delete "type" --prefix $distdir
566
+ npm pkg delete "private" --prefix $distdir
567
+ `,
568
+ 'workspaces/graphql/bin/start.sh': `#!/usr/bin/env sh
569
+
570
+ set -e
571
+
572
+ scriptdir="$PWD/workspaces/graphql";
573
+
574
+ NODE_ENV=production LOG_LEVEL=info tsx $scriptdir/index.ts
575
+ `,
576
+ 'workspaces/graphql/doc.mdx': `import { Meta, Markdown } from '@storybook/addon-docs/blocks';
577
+ import ReadMe from './README.md?raw';
578
+
579
+ <Meta title="Workspaces/Graphql" />
580
+
581
+ <Markdown>{ReadMe}</Markdown>
582
+ `,
583
+ 'workspaces/graphql/index.test.ts': `import { describe, expect, it, jest } from '@jest/globals';
584
+ import type { APIGatewayProxyEvent } from '@vyriy/router';
585
+
586
+ const apiMock = jest.fn((handler) => ({
587
+ handler,
588
+ }));
589
+ const serverMock = jest.fn();
590
+
591
+ jest.mock('@vyriy/handler', () => ({
592
+ api: apiMock,
593
+ }));
594
+
595
+ jest.mock('@vyriy/server', () => ({
596
+ server: serverMock,
597
+ }));
598
+
599
+ describe('workspaces/graphql/index.ts', () => {
600
+ const getPostEvent = (body: string | null, path = '/'): APIGatewayProxyEvent =>
601
+ ({
602
+ body,
603
+ headers: {},
604
+ httpMethod: 'POST',
605
+ path,
606
+ pathParameters: null,
607
+ queryStringParameters: null,
608
+ }) as unknown as APIGatewayProxyEvent;
609
+
610
+ const getEvent = (path = '/'): APIGatewayProxyEvent =>
611
+ ({
612
+ body: null,
613
+ headers: {},
614
+ httpMethod: 'GET',
615
+ path,
616
+ pathParameters: null,
617
+ queryStringParameters: null,
618
+ }) as unknown as APIGatewayProxyEvent;
619
+
620
+ it('starts the server with the API router handler', async () => {
621
+ await import('./index.js');
622
+
623
+ expect(apiMock).toHaveBeenCalledTimes(1);
624
+ expect(serverMock).toHaveBeenCalledTimes(1);
625
+ expect(serverMock).toHaveBeenCalledWith(apiMock.mock.results[0]?.value);
626
+
627
+ const handler = apiMock.mock.calls[0]?.[0] as (event: APIGatewayProxyEvent) => Promise<{
628
+ body: string;
629
+ headers?: Record<string, string>;
630
+ statusCode: number;
631
+ }>;
632
+
633
+ await expect(
634
+ handler(
635
+ getPostEvent(
636
+ JSON.stringify({
637
+ query: '{ hello test { ok message } }',
638
+ }),
639
+ ),
640
+ ),
641
+ ).resolves.toEqual({
642
+ body: JSON.stringify({
643
+ data: {
644
+ hello: 'Hello from GraphQL',
645
+ test: {
646
+ ok: true,
647
+ message: 'GraphQL works',
648
+ },
14
649
  },
650
+ }),
651
+ statusCode: 200,
652
+ });
653
+
654
+ const graphiqlResponse = await handler(getEvent());
655
+
656
+ expect(graphiqlResponse).toEqual({
657
+ body: expect.stringContaining('<title>GraphiQL</title>'),
658
+ headers: {
659
+ 'Content-Type': 'text/html; charset=UTF-8',
660
+ },
661
+ statusCode: 200,
662
+ });
663
+
664
+ await expect(handler(getEvent('/healthcheck'))).resolves.toEqual({
665
+ body: JSON.stringify({
666
+ message: 'Not Found',
667
+ }),
668
+ statusCode: 404,
669
+ });
670
+ });
671
+ });
672
+ `,
673
+ 'workspaces/graphql/index.ts': `import { server } from '@vyriy/server';
674
+ import { api } from '@vyriy/handler';
675
+
676
+ import { router } from '@p/graphql';
677
+
678
+ server(api(async (event) => router.route(event)));
679
+ `,
680
+ 'workspaces/graphql/package.json': `{
681
+ "name": "@w/graphql",
682
+ "type": "module",
683
+ "private": true
684
+ }
685
+ `,
686
+ 'workspaces/graphql/README.md': `# @w/graphql
687
+
688
+ GraphQL runtime workspace for the application.
689
+
690
+ This workspace starts the HTTP server and mounts the reusable \`@p/graphql\` router through \`@vyriy/handler\` and \`@vyriy/server\`.
691
+
692
+ ## Routes
693
+
694
+ - \`GET /\` - serves the embedded GraphiQL page.
695
+ - \`POST /\` - executes GraphQL requests against the package schema.
696
+
697
+ Example request body:
698
+
699
+ \`\`\`json
700
+ {
701
+ "query": "{ hello test { ok message } }"
702
+ }
703
+ \`\`\`
704
+
705
+ ## Development
706
+
707
+ Start the local GraphQL server:
708
+
709
+ \`\`\`bash
710
+ yarn start:graphql
711
+ \`\`\`
712
+
713
+ Build the deployable bundle:
714
+
715
+ \`\`\`bash
716
+ yarn build:graphql
717
+ \`\`\`
718
+
719
+ The build output is written to \`dist/graphql\`.
720
+
721
+ ## Implementation
722
+
723
+ The workspace entrypoint is \`index.ts\`. It wraps the shared router with \`api()\` and passes it to \`server()\`:
724
+
725
+ \`\`\`ts
726
+ server(api(async (event) => router.route(event)));
727
+ \`\`\`
728
+ `,
729
+ 'workspaces/graphql/webpack.config.ts': `import { path } from '@vyriy/path';
730
+ import { ssr, external } from '@vyriy/webpack-config';
731
+
732
+ export default ssr(
733
+ '@w/graphql',
734
+ {
735
+ path: path('dist', 'graphql'),
736
+ filename: 'index.js',
737
+ library: { type: 'commonjs2' },
738
+ },
739
+ (config) => ({
740
+ ...config,
741
+ externals: [external({ allowlist: [/^@p/, /^@w/, /^@vyriy/] })],
742
+ }),
743
+ );
744
+ `,
745
+ }),
746
+ ci: {
747
+ ...base.ci,
15
748
  },
749
+ deploy: {},
16
750
  };
@@ -34,4 +34,9 @@ export declare const presets: {
34
34
  description: string;
35
35
  preset: import("./types.js").Preset;
36
36
  };
37
+ gql: {
38
+ name: string;
39
+ description: string;
40
+ preset: import("./types.js").Preset;
41
+ };
37
42
  };
@@ -5,6 +5,7 @@ import { ssr } from './ssr.js';
5
5
  import { ssg } from './ssg.js';
6
6
  import { spa } from './spa.js';
7
7
  import { rest } from './rest.js';
8
+ import { gql } from './gql.js';
8
9
  export const presets = {
9
10
  base: {
10
11
  name: 'Base',
@@ -41,4 +42,9 @@ export const presets = {
41
42
  description: 'Preset for simple REST API',
42
43
  preset: rest,
43
44
  },
45
+ gql: {
46
+ name: 'GraphQL',
47
+ description: 'Preset for GraphQL API',
48
+ preset: gql,
49
+ },
44
50
  };
@@ -48,7 +48,6 @@ export const rest = {
48
48
  '@vyriy/storybook-config': `^${packageJson.version}`,
49
49
  storybook: packageJson.peerDependencies.storybook,
50
50
  '@vyriy/path': `^${packageJson.version}`,
51
- vyriy: `^${packageJson.version}`,
52
51
  husky: packageJson.peerDependencies.husky,
53
52
  'npm-run-all2': packageJson.peerDependencies['npm-run-all2'],
54
53
  'cross-env': packageJson.peerDependencies['cross-env'],
@@ -57,7 +56,6 @@ export const rest = {
57
56
  '@vyriy/handler': `^${packageJson.version}`,
58
57
  '@vyriy/server': `^${packageJson.version}`,
59
58
  tsx: packageJson.peerDependencies.tsx,
60
- webpack: packageJson.peerDependencies.webpack,
61
59
  'webpack-cli': packageJson.peerDependencies['webpack-cli'],
62
60
  '@vyriy/router': `^${packageJson.version}`,
63
61
  '@vyriy/html': `^${packageJson.version}`,
@@ -51,14 +51,12 @@ export const spa = {
51
51
  '@vyriy/storybook-config': `^${packageJson.version}`,
52
52
  storybook: packageJson.peerDependencies.storybook,
53
53
  '@vyriy/path': `^${packageJson.version}`,
54
- vyriy: `^${packageJson.version}`,
55
54
  husky: packageJson.peerDependencies.husky,
56
55
  'npm-run-all2': packageJson.peerDependencies['npm-run-all2'],
57
56
  'cross-env': packageJson.peerDependencies['cross-env'],
58
57
  rimraf: packageJson.peerDependencies.rimraf,
59
58
  '@vyriy/webpack-config': `^${packageJson.version}`,
60
59
  tsx: packageJson.peerDependencies.tsx,
61
- webpack: packageJson.peerDependencies.webpack,
62
60
  'webpack-cli': packageJson.peerDependencies['webpack-cli'],
63
61
  react: packageJson.peerDependencies.react,
64
62
  'react-dom': packageJson.peerDependencies['react-dom'],
@@ -51,7 +51,6 @@ export const ssg = {
51
51
  '@vyriy/storybook-config': `^${packageJson.version}`,
52
52
  storybook: packageJson.peerDependencies.storybook,
53
53
  '@vyriy/path': `^${packageJson.version}`,
54
- vyriy: `^${packageJson.version}`,
55
54
  husky: packageJson.peerDependencies.husky,
56
55
  'npm-run-all2': packageJson.peerDependencies['npm-run-all2'],
57
56
  'cross-env': packageJson.peerDependencies['cross-env'],
@@ -59,7 +58,6 @@ export const ssg = {
59
58
  '@vyriy/webpack-config': `^${packageJson.version}`,
60
59
  '@vyriy/script': `^${packageJson.version}`,
61
60
  tsx: packageJson.peerDependencies.tsx,
62
- webpack: packageJson.peerDependencies.webpack,
63
61
  'webpack-cli': packageJson.peerDependencies['webpack-cli'],
64
62
  react: packageJson.peerDependencies.react,
65
63
  'react-dom': packageJson.peerDependencies['react-dom'],
@@ -51,7 +51,6 @@ export const ssr = {
51
51
  '@vyriy/storybook-config': `^${packageJson.version}`,
52
52
  storybook: packageJson.peerDependencies.storybook,
53
53
  '@vyriy/path': `^${packageJson.version}`,
54
- vyriy: `^${packageJson.version}`,
55
54
  husky: packageJson.peerDependencies.husky,
56
55
  'npm-run-all2': packageJson.peerDependencies['npm-run-all2'],
57
56
  'cross-env': packageJson.peerDependencies['cross-env'],
@@ -60,7 +59,6 @@ export const ssr = {
60
59
  '@vyriy/handler': `^${packageJson.version}`,
61
60
  '@vyriy/server': `^${packageJson.version}`,
62
61
  tsx: packageJson.peerDependencies.tsx,
63
- webpack: packageJson.peerDependencies.webpack,
64
62
  'webpack-cli': packageJson.peerDependencies['webpack-cli'],
65
63
  react: packageJson.peerDependencies.react,
66
64
  'react-dom': packageJson.peerDependencies['react-dom'],
@@ -1,12 +1,11 @@
1
1
  export type Command = () => Promise<number>;
2
- export type Node = () => {
2
+ export type EnvironmentCheckResult = {
3
3
  ok: boolean;
4
4
  message: string;
5
5
  };
6
- export type Yarn = () => Promise<{
7
- ok: boolean;
8
- message: string;
9
- }>;
6
+ export type Node = () => EnvironmentCheckResult;
7
+ export type Corepack = () => Promise<EnvironmentCheckResult>;
8
+ export type Yarn = () => Promise<EnvironmentCheckResult>;
10
9
  export type CreateOptions = {
11
10
  readonly directory: string;
12
11
  readonly dryRun: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vyriy",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Interactive project master for calm cloud-ready applications.",
5
5
  "type": "module",
6
6
  "bin": "./bin/vyriy.js",
@@ -13,6 +13,7 @@
13
13
  "@types/react-dom": "^19.2.3",
14
14
  "cross-env": "^10.1.0",
15
15
  "eslint": "^10.4.0",
16
+ "graphql": "^16.14.0",
16
17
  "husky": "^9.1.7",
17
18
  "jest": "^30.4.2",
18
19
  "npm-run-all2": "^9.0.1",
@@ -25,7 +26,6 @@
25
26
  "stylelint": "^17.12.0",
26
27
  "tsx": "^4.22.3",
27
28
  "typescript": "^6.0.3",
28
- "webpack": "^5.107.2",
29
29
  "webpack-cli": "^7.0.2"
30
30
  },
31
31
  "peerDependenciesMeta": {
@@ -41,6 +41,9 @@
41
41
  "eslint": {
42
42
  "optional": true
43
43
  },
44
+ "graphql": {
45
+ "optional": true
46
+ },
44
47
  "husky": {
45
48
  "optional": true
46
49
  },
@@ -77,9 +80,6 @@
77
80
  "typescript": {
78
81
  "optional": true
79
82
  },
80
- "webpack": {
81
- "optional": true
82
- },
83
83
  "webpack-cli": {
84
84
  "optional": true
85
85
  }