vyriy 0.4.3 → 0.4.6

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.
package/README.md CHANGED
@@ -78,22 +78,17 @@ Prepares every package inside the `dist/` directory for npm publishing:
78
78
 
79
79
  Registered presets:
80
80
 
81
- | Key | Description |
82
- | --------- | ---------------------------------------------- |
83
- | `base` | Minimal monorepo with config only |
84
- | `library` | Workspaces layout with a sample React package |
85
- | `api` | Backend API workspace with server/build setup |
86
- | `ssr` | Server-rendered React API with CMS placeholder |
87
-
88
- Presets in progress:
89
-
90
- | Key | Direction |
91
- | ------ | ------------------------------ |
92
- | `rest` | REST API project |
93
- | `gql` | GraphQL API project |
94
- | `ssg` | Static site generation project |
95
- | `spa` | Single-page application |
96
- | `mfe` | Micro-frontend project |
81
+ | Key | Description |
82
+ | --------- | ------------------------------------------------- |
83
+ | `base` | Preset to create minimal monorepo with configs |
84
+ | `library` | Preset to create js/react library |
85
+ | `api` | Preset to create simple API |
86
+ | `ssr` | Preset to create Server Side Rendering (SSR) API |
87
+ | `ssg` | Preset to create Static site generation (SSG) |
88
+ | `spa` | Preset to create Single-page application (SPA) |
89
+ | `rest` | Preset to create simple REST API |
90
+ | `gql` | Preset to create GraphQL API |
91
+ | `mfe` | Preset to create Micro-frontend (MFE) application |
97
92
 
98
93
  Registered presets are selectable by the wizard. In-progress presets exist as
99
94
  source modules and are expected to become selectable as their generated project
@@ -109,6 +104,8 @@ Provider selections add files to the generated project.
109
104
  | `library` | `gitlab`, `github` | none |
110
105
  | `api` | `gitlab`, `github` | none |
111
106
  | `ssr` | `gitlab`, `github` | none |
107
+ | `ssg` | `gitlab`, `github` | none |
108
+ | `spa` | `gitlab`, `github` | none |
112
109
 
113
110
  ## API
114
111
 
@@ -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" | "api" | "library";
5
+ preset: "ssr" | "base" | "rest" | "api" | "library" | "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;
@@ -36,7 +36,7 @@ export const api = {
36
36
  'test:jest': 'jest',
37
37
  postinstall: 'husky',
38
38
  },
39
- devDependencies: {
39
+ dependencies: {
40
40
  '@vyriy/typescript-config': `^${packageJson.version}`,
41
41
  typescript: packageJson.peerDependencies.typescript,
42
42
  '@vyriy/prettier-config': `^${packageJson.version}`,
@@ -19,4 +19,19 @@ export declare const presets: {
19
19
  description: string;
20
20
  preset: import("./types.js").Preset;
21
21
  };
22
+ ssg: {
23
+ name: string;
24
+ description: string;
25
+ preset: import("./types.js").Preset;
26
+ };
27
+ spa: {
28
+ name: string;
29
+ description: string;
30
+ preset: import("./types.js").Preset;
31
+ };
32
+ rest: {
33
+ name: string;
34
+ description: string;
35
+ preset: import("./types.js").Preset;
36
+ };
22
37
  };
@@ -2,6 +2,9 @@ import { base } from './base.js';
2
2
  import { library } from './library.js';
3
3
  import { api } from './api.js';
4
4
  import { ssr } from './ssr.js';
5
+ import { ssg } from './ssg.js';
6
+ import { spa } from './spa.js';
7
+ import { rest } from './rest.js';
5
8
  export const presets = {
6
9
  base: {
7
10
  name: 'Base',
@@ -23,4 +26,19 @@ export const presets = {
23
26
  description: 'Preset to generate simple Server Side Rendering (SSR) API',
24
27
  preset: ssr,
25
28
  },
29
+ ssg: {
30
+ name: 'SSG',
31
+ description: 'Preset for Static Site Generation (SSG)',
32
+ preset: ssg,
33
+ },
34
+ spa: {
35
+ name: 'SPA',
36
+ description: 'Preset for Single Page Application (SPA)',
37
+ preset: spa,
38
+ },
39
+ rest: {
40
+ name: 'REST',
41
+ description: 'Preset for simple REST API',
42
+ preset: rest,
43
+ },
26
44
  };
@@ -44,7 +44,7 @@ export const library = {
44
44
  'test:jest': 'jest',
45
45
  postinstall: 'husky',
46
46
  },
47
- devDependencies: {
47
+ dependencies: {
48
48
  '@vyriy/typescript-config': `^${packageJson.version}`,
49
49
  typescript: packageJson.peerDependencies.typescript,
50
50
  '@vyriy/prettier-config': `^${packageJson.version}`,
@@ -1,16 +1,346 @@
1
+ import packageJson from '../../../package.json' with { type: 'json' };
1
2
  import { base } from './base.js';
2
3
  export const rest = {
3
4
  files: (options) => ({
4
5
  ...base.files(options),
5
- }),
6
- ci: {
7
- github: {
8
- '.gitlab-ci.yml': 'code',
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
+ 'workspaces/*',
19
+ ],
20
+ scripts: {
21
+ storybook: 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook dev -p 6006 --disable-telemetry',
22
+ check: 'run-s lint build test',
23
+ fix: "run-s 'fix:*'",
24
+ start: "run-p 'start:*'",
25
+ lint: "run-s 'lint:*'",
26
+ build: "run-s 'build:*'",
27
+ test: "run-s 'test:*'",
28
+ 'fix:prettier': 'prettier . --write',
29
+ 'fix:eslint': 'eslint . --fix',
30
+ 'start:api': 'sh workspaces/api/bin/start.sh',
31
+ 'lint:ts': 'tsc',
32
+ 'lint:prettier': 'prettier . --check',
33
+ 'lint:eslint': 'eslint .',
34
+ 'build:api': 'rimraf dist && sh workspaces/api/bin/build.sh',
35
+ 'build:storybook': 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook build --quiet --disable-telemetry',
36
+ 'test:jest': 'jest',
37
+ postinstall: 'husky',
38
+ },
39
+ dependencies: {
40
+ '@vyriy/typescript-config': `^${packageJson.version}`,
41
+ typescript: packageJson.peerDependencies.typescript,
42
+ '@vyriy/prettier-config': `^${packageJson.version}`,
43
+ prettier: packageJson.peerDependencies.prettier,
44
+ '@vyriy/eslint-config': `^${packageJson.version}`,
45
+ eslint: packageJson.peerDependencies.eslint,
46
+ '@vyriy/jest-config': `^${packageJson.version}`,
47
+ jest: packageJson.peerDependencies.jest,
48
+ '@vyriy/storybook-config': `^${packageJson.version}`,
49
+ storybook: packageJson.peerDependencies.storybook,
50
+ '@vyriy/path': `^${packageJson.version}`,
51
+ vyriy: `^${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
+ tsx: packageJson.peerDependencies.tsx,
60
+ webpack: packageJson.peerDependencies.webpack,
61
+ 'webpack-cli': packageJson.peerDependencies['webpack-cli'],
62
+ '@vyriy/router': `^${packageJson.version}`,
63
+ '@vyriy/html': `^${packageJson.version}`,
64
+ },
65
+ }, null, 2) + '\n',
66
+ 'workspaces/api/bin/build.sh': `#!/usr/bin/env sh
67
+
68
+ set -e
69
+
70
+ scriptdir="$PWD/workspaces/api";
71
+
72
+ NODE_ENV=production npx webpack --config $scriptdir/webpack.config.ts
73
+
74
+ cp $scriptdir/package.json dist/api/package.json
75
+ npm pkg delete "type" --prefix dist/api
76
+ npm pkg delete "private" --prefix dist/api
77
+ `,
78
+ 'workspaces/api/bin/start.sh': `#!/usr/bin/env sh
79
+
80
+ set -e
81
+
82
+ scriptdir="$PWD/workspaces/api";
83
+
84
+ NODE_ENV=production LOG_LEVEL=info tsx $scriptdir/index.ts
85
+ `,
86
+ 'workspaces/api/doc.mdx': `import { Meta, Markdown } from '@storybook/addon-docs/blocks';
87
+ import ReadMe from './README.md?raw';
88
+
89
+ <Meta title="Workspaces/API" />
90
+
91
+ <Markdown>{ReadMe}</Markdown>
92
+ `,
93
+ 'workspaces/api/README.md': `# ${options.name} API\n\n${options.description}\n`,
94
+ 'workspaces/api/webpack.config.ts': `import { path } from '@vyriy/path';
95
+ import { ssr, external } from '@vyriy/webpack-config';
96
+
97
+ export default ssr(
98
+ '@w/api',
99
+ {
100
+ path: path('dist', 'api'),
101
+ filename: 'index.js',
102
+ library: { type: 'commonjs2' },
103
+ },
104
+ (config) => ({
105
+ ...config,
106
+ externals: [external({ allowlist: [/^@p/, /^@w/, /^@vyriy/] })],
107
+ }),
108
+ );
109
+ `,
110
+ 'workspaces/api/package.json': JSON.stringify({
111
+ name: '@w/api',
112
+ type: 'module',
113
+ private: true,
114
+ }, null, 2) + '\n',
115
+ 'workspaces/api/index.ts': `import { server } from '@vyriy/server';
116
+ import { api } from '@vyriy/handler';
117
+ import { createRouter } from '@vyriy/router';
118
+ import { html, minify } from '@vyriy/html';
119
+
120
+ const router = createRouter();
121
+
122
+ router.get('/api/test', async () => {
123
+ return Promise.resolve({
124
+ headers: {
125
+ 'content-type': 'application/json',
126
+ },
127
+ body: JSON.stringify({ test: 'ok' }),
128
+ });
129
+ });
130
+
131
+ router.get('/openapi.json', async () => {
132
+ return Promise.resolve({
133
+ headers: {
134
+ 'content-type': 'application/json',
135
+ },
136
+ body: JSON.stringify({
137
+ openapi: '3.0.0',
138
+ info: {
139
+ title: 'REST API',
140
+ description: 'A minimal example of an OpenAPI definition in JSON format.',
141
+ version: '1.0.0',
142
+ },
143
+ servers: [
144
+ {
145
+ url: 'http://localhost:3000',
146
+ description: 'Local server',
147
+ },
148
+ ],
149
+ paths: {
150
+ '/api/test': {
151
+ get: {
152
+ summary: 'Test endpoint',
153
+ operationId: 'getTest',
154
+ responses: {
155
+ '200': {
156
+ description: 'A successful test response',
157
+ content: {
158
+ 'application/json': {
159
+ schema: {
160
+ $ref: '#/components/schemas/TestResponse',
161
+ },
162
+ example: {
163
+ test: 'ok',
164
+ },
165
+ },
166
+ },
167
+ },
168
+ },
169
+ },
170
+ },
171
+ },
172
+ components: {
173
+ schemas: {
174
+ TestResponse: {
175
+ type: 'object',
176
+ required: ['test'],
177
+ properties: {
178
+ test: {
179
+ type: 'string',
180
+ example: 'ok',
181
+ },
182
+ },
183
+ },
9
184
  },
185
+ },
186
+ }),
187
+ });
188
+ });
189
+
190
+ router.get('/', async () => {
191
+ return Promise.resolve({
192
+ headers: {
193
+ 'content-type': 'text/html; charset=utf-8',
10
194
  },
11
- deploy: {
12
- docker: {
13
- Dockerfile: 'code',
195
+ body: minify(
196
+ html({
197
+ title: '<title>REST API</title>',
198
+ meta: '<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />',
199
+ body: [
200
+ '<div id="app"></div>',
201
+ '<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>',
202
+ "<script>Scalar.createApiReference('#app', { url: '/openapi.json' })</script>",
203
+ ].join(''),
204
+ }),
205
+ ),
206
+ });
207
+ });
208
+
209
+ server(api(async (event) => router.route(event)));
210
+ `,
211
+ 'workspaces/api/index.test.ts': `import { describe, expect, it, jest } from '@jest/globals';
212
+ import type { APIGatewayProxyEvent } from '@vyriy/router';
213
+
214
+ const apiMock = jest.fn((handler) => ({
215
+ handler,
216
+ }));
217
+ const serverMock = jest.fn();
218
+
219
+ jest.mock('@vyriy/handler', () => ({
220
+ api: apiMock,
221
+ }));
222
+
223
+ jest.mock('@vyriy/server', () => ({
224
+ server: serverMock,
225
+ }));
226
+
227
+ describe('workspaces/api/index.ts', () => {
228
+ const getEvent = (path: string): APIGatewayProxyEvent =>
229
+ ({
230
+ body: null,
231
+ headers: {},
232
+ httpMethod: 'GET',
233
+ path,
234
+ pathParameters: null,
235
+ queryStringParameters: null,
236
+ }) as APIGatewayProxyEvent;
237
+
238
+ it('starts the server with the API router handler', async () => {
239
+ await import('./index.js');
240
+
241
+ expect(apiMock).toHaveBeenCalledTimes(1);
242
+ expect(serverMock).toHaveBeenCalledTimes(1);
243
+ expect(serverMock).toHaveBeenCalledWith(apiMock.mock.results[0]?.value);
244
+
245
+ const handler = apiMock.mock.calls[0]?.[0] as (event: APIGatewayProxyEvent) => Promise<{
246
+ body: string;
247
+ headers?: Record<string, string>;
248
+ statusCode: number;
249
+ }>;
250
+
251
+ await expect(handler(getEvent('/api/test'))).resolves.toEqual({
252
+ body: JSON.stringify({
253
+ test: 'ok',
254
+ }),
255
+ headers: {
256
+ 'content-type': 'application/json',
257
+ },
258
+ statusCode: 200,
259
+ });
260
+
261
+ const openApiResponse = await handler(getEvent('/openapi.json'));
262
+
263
+ expect(openApiResponse).toEqual({
264
+ body: expect.any(String),
265
+ headers: {
266
+ 'content-type': 'application/json',
267
+ },
268
+ statusCode: 200,
269
+ });
270
+ expect(JSON.parse(openApiResponse.body)).toEqual({
271
+ components: {
272
+ schemas: {
273
+ TestResponse: {
274
+ properties: {
275
+ test: {
276
+ example: 'ok',
277
+ type: 'string',
278
+ },
279
+ },
280
+ required: ['test'],
281
+ type: 'object',
282
+ },
283
+ },
284
+ },
285
+ info: {
286
+ description: 'A minimal example of an OpenAPI definition in JSON format.',
287
+ title: 'REST API',
288
+ version: '1.0.0',
289
+ },
290
+ openapi: '3.0.0',
291
+ paths: {
292
+ '/api/test': {
293
+ get: {
294
+ operationId: 'getTest',
295
+ responses: {
296
+ '200': {
297
+ content: {
298
+ 'application/json': {
299
+ example: {
300
+ test: 'ok',
301
+ },
302
+ schema: {
303
+ $ref: '#/components/schemas/TestResponse',
304
+ },
305
+ },
306
+ },
307
+ description: 'A successful test response',
308
+ },
309
+ },
310
+ summary: 'Test endpoint',
311
+ },
312
+ },
313
+ },
314
+ servers: [
315
+ {
316
+ description: 'Local server',
317
+ url: 'http://localhost:3000',
14
318
  },
319
+ ],
320
+ });
321
+
322
+ const docsResponse = await handler(getEvent('/'));
323
+
324
+ expect(docsResponse).toEqual({
325
+ body: expect.stringContaining("Scalar.createApiReference('#app', { url: '/openapi.json' })"),
326
+ headers: {
327
+ 'content-type': 'text/html; charset=utf-8',
328
+ },
329
+ statusCode: 200,
330
+ });
331
+
332
+ await expect(handler(getEvent('/healthcheck'))).resolves.toEqual({
333
+ body: JSON.stringify({
334
+ message: 'Not Found',
335
+ }),
336
+ statusCode: 404,
337
+ });
338
+ });
339
+ });
340
+ `,
341
+ }),
342
+ ci: {
343
+ ...base.ci,
15
344
  },
345
+ deploy: {},
16
346
  };
@@ -1,16 +1,250 @@
1
+ import packageJson from '../../../package.json' with { type: 'json' };
1
2
  import { base } from './base.js';
2
3
  export const spa = {
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
+ 'fix:stylelint': 'stylelint "**/*.{css,scss}" --fix',
32
+ 'start:spa': 'sh workspaces/spa/bin/start.sh',
33
+ 'lint:ts': 'tsc',
34
+ 'lint:prettier': 'prettier . --check',
35
+ 'lint:eslint': 'eslint .',
36
+ 'lint:stylelint': 'stylelint "**/*.{css,scss}"',
37
+ 'build:spa': 'rimraf dist && sh workspaces/spa/bin/build.sh',
38
+ 'build:storybook': 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook build --quiet --disable-telemetry',
39
+ 'test:jest': 'jest',
40
+ postinstall: 'husky',
41
+ },
42
+ dependencies: {
43
+ '@vyriy/typescript-config': `^${packageJson.version}`,
44
+ typescript: packageJson.peerDependencies.typescript,
45
+ '@vyriy/prettier-config': `^${packageJson.version}`,
46
+ prettier: packageJson.peerDependencies.prettier,
47
+ '@vyriy/eslint-config': `^${packageJson.version}`,
48
+ eslint: packageJson.peerDependencies.eslint,
49
+ '@vyriy/jest-config': `^${packageJson.version}`,
50
+ jest: packageJson.peerDependencies.jest,
51
+ '@vyriy/storybook-config': `^${packageJson.version}`,
52
+ storybook: packageJson.peerDependencies.storybook,
53
+ '@vyriy/path': `^${packageJson.version}`,
54
+ vyriy: `^${packageJson.version}`,
55
+ husky: packageJson.peerDependencies.husky,
56
+ 'npm-run-all2': packageJson.peerDependencies['npm-run-all2'],
57
+ 'cross-env': packageJson.peerDependencies['cross-env'],
58
+ rimraf: packageJson.peerDependencies.rimraf,
59
+ '@vyriy/webpack-config': `^${packageJson.version}`,
60
+ tsx: packageJson.peerDependencies.tsx,
61
+ webpack: packageJson.peerDependencies.webpack,
62
+ 'webpack-cli': packageJson.peerDependencies['webpack-cli'],
63
+ react: packageJson.peerDependencies.react,
64
+ 'react-dom': packageJson.peerDependencies['react-dom'],
65
+ '@types/react': packageJson.peerDependencies['@types/react'],
66
+ '@types/react-dom': packageJson.peerDependencies['@types/react-dom'],
67
+ '@vyriy/stylelint-config': `^${packageJson.version}`,
68
+ '@vyriy/cn': `^${packageJson.version}`,
69
+ '@vyriy/html': `^${packageJson.version}`,
70
+ stylelint: packageJson.peerDependencies.stylelint,
71
+ '@vyriy/browserslist-config': `^${packageJson.version}`,
72
+ },
73
+ }, null, 2) + '\n',
74
+ 'stylelint.config.ts': "export { default } from '@vyriy/stylelint-config';\n",
75
+ 'assets.d.ts': "declare module '*.scss';\n",
76
+ '.browserslistrc': `[development]
77
+ extends @vyriy/browserslist-config
78
+
79
+ [ssr]
80
+ extends @vyriy/browserslist-config
81
+
82
+ [production]
83
+ extends @vyriy/browserslist-config
84
+
85
+ [modern]
86
+ extends @vyriy/browserslist-config
87
+ `,
88
+ 'packages/components/package.json': JSON.stringify({
89
+ name: '@p/components',
90
+ private: true,
91
+ type: 'module',
92
+ }, null, 2) + '\n',
93
+ 'packages/components/index.ts': "export * from './page/index.js';\n",
94
+ 'packages/components/index.test.tsx': `import { describe, expect, it } from '@jest/globals';
95
+
96
+ import { Page } from './index.js';
97
+ import { Page as PageImplementation } from './page/index.js';
98
+
99
+ describe('packages/components/page', () => {
100
+ it('re-exports the page component', () => {
101
+ expect(Page).toBe(PageImplementation);
102
+ });
103
+ });
104
+ `,
105
+ 'packages/components/page/index.ts': `export * from './page.js';
106
+ export type * from './types.js';
107
+ `,
108
+ 'packages/components/page/index.test.ts': `import { describe, expect, it } from '@jest/globals';
109
+
110
+ import { Page } from './index.js';
111
+ import { Page as PageImplementation } from './page.js';
112
+
113
+ describe('packages/components/page', () => {
114
+ it('re-exports the page component', () => {
115
+ expect(Page).toBe(PageImplementation);
116
+ });
117
+ });
118
+ `,
119
+ 'packages/components/page/types.ts': `import { FC } from 'react';
120
+
121
+ export type PageProps = {
122
+ content: string;
123
+ };
124
+
125
+ export type PageType = FC<PageProps>;
126
+ `,
127
+ 'packages/components/page/page.tsx': `import type { PageType } from './types.js';
128
+
129
+ export const Page: PageType = ({ content }) => <div className="content">{content}</div>;
130
+ `,
131
+ 'packages/components/page/styles.scss': `.content {
132
+ display: block;
133
+ }
134
+ `,
135
+ 'packages/components/page/page.test.tsx': `import { renderToStaticMarkup } from 'react-dom/server';
136
+ import { describe, expect, it } from '@jest/globals';
137
+
138
+ import { Page } from './page.js';
139
+
140
+ describe('packages/components/page/page', () => {
141
+ it('renders content inside the page content container', () => {
142
+ expect(renderToStaticMarkup(<Page content="Page body" />)).toBe('<div class="content">Page body</div>');
143
+ });
144
+ });
145
+ `,
146
+ 'workspaces/spa/bin/build.sh': `#!/usr/bin/env sh
147
+
148
+ set -e
149
+
150
+ scriptdir="$PWD/workspaces/spa";
151
+
152
+ NODE_ENV=production npx webpack --config $scriptdir/webpack.config.ts
153
+ `,
154
+ 'workspaces/spa/bin/start.sh': `#!/usr/bin/env sh
155
+
156
+ set -e
157
+
158
+ scriptdir="$PWD/workspaces/spa";
159
+
160
+ npx webpack serve --open --config $scriptdir/webpack.config.ts
161
+ `,
162
+ 'workspaces/spa/doc.mdx': `import { Meta, Markdown } from '@storybook/addon-docs/blocks';
163
+ import ReadMe from './README.md?raw';
164
+
165
+ <Meta title="Workspaces/SPA" />
166
+
167
+ <Markdown>{ReadMe}</Markdown>
168
+ `,
169
+ 'workspaces/spa/README.md': `# ${options.name} SPA\n\n${options.description}\n`,
170
+ 'workspaces/spa/webpack.config.ts': `import { csr, html } from '@vyriy/webpack-config';
171
+ import { path } from '@vyriy/path';
172
+
173
+ export default csr(
174
+ '@w/spa',
175
+ {
176
+ path: path('dist', 'spa'),
177
+ filename: 'index.js',
178
+ },
179
+ (config) => {
180
+ return {
181
+ ...config,
182
+ plugins: [
183
+ ...(config.plugins ?? []),
184
+ html({
185
+ title: '<title>SPA</title>',
186
+ body: '<div id="root"></div>',
187
+ }),
188
+ ],
189
+ };
190
+ },
191
+ );
192
+ `,
193
+ 'workspaces/spa/package.json': JSON.stringify({
194
+ name: '@w/spa',
195
+ type: 'module',
196
+ private: true,
197
+ }, null, 2) + '\n',
198
+ 'workspaces/spa/index.tsx': `import { createRoot } from 'react-dom/client';
199
+
200
+ import { Page } from '@p/components';
201
+
202
+ import '@p/components/page/styles.scss';
203
+
204
+ createRoot(document.getElementById('root')!).render(<Page content="Test content" />);
205
+ `,
206
+ 'workspaces/spa/index.test.tsx': `import type { ReactElement, ReactNode } from 'react';
207
+ import { describe, expect, it, jest } from '@jest/globals';
208
+
209
+ const renderMock = jest.fn<(children: ReactNode) => void>();
210
+ const createRootMock = jest.fn<(container: Element | DocumentFragment) => { render: typeof renderMock }>(() => ({
211
+ render: renderMock,
212
+ }));
213
+ const PageMock = jest.fn(({ content }: { content: string }) => <div>{content}</div>);
214
+
215
+ jest.mock('react-dom/client', () => ({
216
+ createRoot: createRootMock,
217
+ }));
218
+
219
+ jest.mock('@p/components', () => ({
220
+ Page: PageMock,
221
+ }));
222
+
223
+ describe('workspaces/spa/index.tsx', () => {
224
+ it('mounts the page component into the root element', async () => {
225
+ document.body.innerHTML = '<div id="root"></div>';
226
+ const rootElement = document.getElementById('root');
227
+
228
+ if (!rootElement) {
229
+ throw new Error('Expected root element to exist.');
230
+ }
231
+
232
+ await import('./index.js');
233
+
234
+ expect(createRootMock).toHaveBeenCalledTimes(1);
235
+ expect(createRootMock.mock.calls[0]?.[0]).toBe(rootElement);
236
+ expect(renderMock).toHaveBeenCalledTimes(1);
237
+
238
+ const renderedElement = renderMock.mock.calls[0]?.[0] as ReactElement<{ content: string }>;
239
+
240
+ expect(renderedElement.type).toBe(PageMock);
241
+ expect(renderedElement.props.content).toBe('Test content');
242
+ });
243
+ });
244
+ `,
5
245
  }),
6
246
  ci: {
7
- github: {
8
- '.gitlab-ci.yml': 'code',
9
- },
10
- },
11
- deploy: {
12
- docker: {
13
- Dockerfile: 'code',
14
- },
247
+ ...base.ci,
15
248
  },
249
+ deploy: {},
16
250
  };
@@ -1,16 +1,316 @@
1
+ import packageJson from '../../../package.json' with { type: 'json' };
1
2
  import { base } from './base.js';
2
3
  export const ssg = {
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
+ 'fix:stylelint': 'stylelint "**/*.{css,scss}" --fix',
32
+ 'start:ssg': 'sh workspaces/ssg/bin/start.sh',
33
+ 'lint:ts': 'tsc',
34
+ 'lint:prettier': 'prettier . --check',
35
+ 'lint:eslint': 'eslint .',
36
+ 'lint:stylelint': 'stylelint "**/*.{css,scss}"',
37
+ 'build:ssg': 'rimraf dist && sh workspaces/ssg/bin/build.sh',
38
+ 'build:storybook': 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook build --quiet --disable-telemetry',
39
+ 'test:jest': 'jest',
40
+ postinstall: 'husky',
41
+ },
42
+ dependencies: {
43
+ '@vyriy/typescript-config': `^${packageJson.version}`,
44
+ typescript: packageJson.peerDependencies.typescript,
45
+ '@vyriy/prettier-config': `^${packageJson.version}`,
46
+ prettier: packageJson.peerDependencies.prettier,
47
+ '@vyriy/eslint-config': `^${packageJson.version}`,
48
+ eslint: packageJson.peerDependencies.eslint,
49
+ '@vyriy/jest-config': `^${packageJson.version}`,
50
+ jest: packageJson.peerDependencies.jest,
51
+ '@vyriy/storybook-config': `^${packageJson.version}`,
52
+ storybook: packageJson.peerDependencies.storybook,
53
+ '@vyriy/path': `^${packageJson.version}`,
54
+ vyriy: `^${packageJson.version}`,
55
+ husky: packageJson.peerDependencies.husky,
56
+ 'npm-run-all2': packageJson.peerDependencies['npm-run-all2'],
57
+ 'cross-env': packageJson.peerDependencies['cross-env'],
58
+ rimraf: packageJson.peerDependencies.rimraf,
59
+ '@vyriy/webpack-config': `^${packageJson.version}`,
60
+ '@vyriy/script': `^${packageJson.version}`,
61
+ tsx: packageJson.peerDependencies.tsx,
62
+ webpack: packageJson.peerDependencies.webpack,
63
+ 'webpack-cli': packageJson.peerDependencies['webpack-cli'],
64
+ react: packageJson.peerDependencies.react,
65
+ 'react-dom': packageJson.peerDependencies['react-dom'],
66
+ '@types/react': packageJson.peerDependencies['@types/react'],
67
+ '@types/react-dom': packageJson.peerDependencies['@types/react-dom'],
68
+ '@vyriy/stylelint-config': `^${packageJson.version}`,
69
+ '@vyriy/cn': `^${packageJson.version}`,
70
+ '@vyriy/html': `^${packageJson.version}`,
71
+ stylelint: packageJson.peerDependencies.stylelint,
72
+ sass: packageJson.peerDependencies.sass,
73
+ },
74
+ }, null, 2) + '\n',
75
+ 'stylelint.config.ts': "export { default } from '@vyriy/stylelint-config';\n",
76
+ 'assets.d.ts': "declare module '*.scss';\n",
77
+ 'packages/components/package.json': JSON.stringify({
78
+ name: '@p/components',
79
+ private: true,
80
+ type: 'module',
81
+ }, null, 2) + '\n',
82
+ 'packages/components/index.ts': "export * from './page/index.js';\n",
83
+ 'packages/components/index.test.tsx': `import { describe, expect, it } from '@jest/globals';
84
+
85
+ import { Page } from './index.js';
86
+ import { Page as PageImplementation } from './page/index.js';
87
+
88
+ describe('packages/components/page', () => {
89
+ it('re-exports the page component', () => {
90
+ expect(Page).toBe(PageImplementation);
91
+ });
92
+ });
93
+ `,
94
+ 'packages/components/page/index.ts': `export * from './page.js';
95
+ export type * from './types.js';
96
+ `,
97
+ 'packages/components/page/index.test.ts': `import { describe, expect, it } from '@jest/globals';
98
+
99
+ import { Page } from './index.js';
100
+ import { Page as PageImplementation } from './page.js';
101
+
102
+ describe('packages/components/page', () => {
103
+ it('re-exports the page component', () => {
104
+ expect(Page).toBe(PageImplementation);
105
+ });
106
+ });
107
+ `,
108
+ 'packages/components/page/types.ts': `import { FC } from 'react';
109
+
110
+ export type PageProps = {
111
+ content: string;
112
+ };
113
+
114
+ export type PageType = FC<PageProps>;
115
+ `,
116
+ 'packages/components/page/page.tsx': `import type { PageType } from './types.js';
117
+
118
+ export const Page: PageType = ({ content }) => <div className="content">{content}</div>;
119
+ `,
120
+ 'packages/components/page/styles.scss': `.content {
121
+ display: block;
122
+ }
123
+ `,
124
+ 'packages/components/page/page.test.tsx': `import { renderToStaticMarkup } from 'react-dom/server';
125
+ import { describe, expect, it } from '@jest/globals';
126
+
127
+ import { Page } from './page.js';
128
+
129
+ describe('packages/components/page/page', () => {
130
+ it('renders content inside the page content container', () => {
131
+ expect(renderToStaticMarkup(<Page content="Page body" />)).toBe('<div class="content">Page body</div>');
132
+ });
133
+ });
134
+ `,
135
+ 'packages/services/package.json': JSON.stringify({
136
+ name: '@p/services',
137
+ private: true,
138
+ type: 'module',
139
+ }, null, 2) + '\n',
140
+ 'packages/services/cms/index.ts': `export const cms = {
141
+ getContent: async () => {
142
+ // Placeholder for fetching content from a CMS
143
+ return Promise.resolve({
144
+ title: 'Sample Content',
145
+ body: 'This is a sample content fetched from the CMS.',
146
+ });
147
+ },
148
+ };
149
+ `,
150
+ 'packages/services/cms/index.test.ts': `import { describe, expect, it } from '@jest/globals';
151
+
152
+ import { cms } from './index.js';
153
+
154
+ describe('packages/services/cms', () => {
155
+ it('returns content for rendering a page', async () => {
156
+ await expect(cms.getContent()).resolves.toEqual({
157
+ title: 'Sample Content',
158
+ body: 'This is a sample content fetched from the CMS.',
159
+ });
160
+ });
161
+ });
162
+ `,
163
+ 'workspaces/ssg/bin/build.sh': `#!/usr/bin/env sh
164
+
165
+ set -e
166
+
167
+ scriptdir="$PWD/workspaces/ssg";
168
+ distdir="$PWD/dist/ssg";
169
+
170
+ NODE_ENV=production npx webpack --config $scriptdir/webpack.config.ts
171
+
172
+ yarn exec sass packages/components/page/styles.scss "$distdir/styles.css" --no-source-map --style=compressed
173
+ cp $scriptdir/package.json "$distdir/package.json"
174
+ npm pkg delete "type" --prefix "$distdir"
175
+ npm pkg delete "private" --prefix "$distdir"
176
+ `,
177
+ 'workspaces/ssg/bin/start.sh': `#!/usr/bin/env sh
178
+
179
+ set -e
180
+
181
+ scriptdir="$PWD/workspaces/ssg";
182
+ distdir="$PWD/dist/ssg";
183
+
184
+ mkdir -p "$distdir"
185
+ yarn exec sass packages/components/page/styles.scss "$distdir/styles.css" --no-source-map
186
+
187
+ PROJECT_CWD="$distdir" NODE_ENV=production LOG_LEVEL=info "$PWD/node_modules/.bin/tsx" $scriptdir/index.tsx
188
+ `,
189
+ 'workspaces/ssg/doc.mdx': `import { Meta, Markdown } from '@storybook/addon-docs/blocks';
190
+ import ReadMe from './README.md?raw';
191
+
192
+ <Meta title="Workspaces/SSG" />
193
+
194
+ <Markdown>{ReadMe}</Markdown>
195
+ `,
196
+ 'workspaces/ssg/README.md': `# ${options.name} SSG\n\n${options.description}\n`,
197
+ 'workspaces/ssg/webpack.config.ts': `import { path } from '@vyriy/path';
198
+ import { ssr, external } from '@vyriy/webpack-config';
199
+
200
+ export default ssr(
201
+ '@w/ssg',
202
+ {
203
+ path: path('dist', 'ssg'),
204
+ filename: 'index.js',
205
+ library: { type: 'commonjs2' },
206
+ },
207
+ (config) => ({
208
+ ...config,
209
+ externals: [external({ allowlist: [/^@p/, /^@w/, /^@vyriy/] })],
210
+ }),
211
+ );
212
+ `,
213
+ 'workspaces/ssg/package.json': JSON.stringify({
214
+ name: '@w/ssg',
215
+ type: 'module',
216
+ private: true,
217
+ }, null, 2) + '\n',
218
+ 'workspaces/ssg/index.tsx': `import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
219
+ import { renderToString } from 'react-dom/server';
220
+
221
+ import { script } from '@vyriy/script';
222
+ import { html, minify } from '@vyriy/html';
223
+ import { path } from '@vyriy/path';
224
+
225
+ import { cms } from '@p/services/cms';
226
+ import { Page } from '@p/components';
227
+
228
+ const dashboardStyles = readFileSync(path('styles.css'), 'utf8');
229
+ const staticPath = path('static');
230
+
231
+ void script(async () => {
232
+ const content = await cms.getContent();
233
+
234
+ mkdirSync(staticPath, { recursive: true });
235
+
236
+ writeFileSync(
237
+ path(staticPath, 'index.html'),
238
+ minify(
239
+ html({
240
+ htmlAttributes: 'lang="en"',
241
+ title: \`<title>\${content.title}</title>\`,
242
+ meta: '<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />',
243
+ style: \`<style>\${dashboardStyles.trim()}</style>\`,
244
+ body: renderToString(<Page content={content.body} />),
245
+ }),
246
+ ),
247
+ );
248
+ });
249
+ `,
250
+ 'workspaces/ssg/index.test.tsx': `import { describe, expect, it, jest } from '@jest/globals';
251
+
252
+ const getContentMock = jest.fn(() =>
253
+ Promise.resolve({
254
+ title: 'Sample Content',
255
+ body: 'This is a sample content fetched from the CMS.',
256
+ }),
257
+ );
258
+ let scriptPromise: Promise<void> | undefined;
259
+ const scriptMock = jest.fn((handler: () => Promise<void>) => {
260
+ scriptPromise = handler();
261
+
262
+ return scriptPromise;
263
+ });
264
+ const nodeFs = jest.requireActual<typeof import('node:fs')>('node:fs');
265
+ const mkdirSyncMock = jest.fn();
266
+ const readFileSyncMock = jest.fn<(path: string | URL, encoding: 'utf8') => string>(
267
+ () => '.content { display: block; }',
268
+ );
269
+ const writeFileSyncMock = jest.fn();
270
+
271
+ jest.mock('node:fs', () => ({
272
+ ...nodeFs,
273
+ mkdirSync: mkdirSyncMock,
274
+ readFileSync: readFileSyncMock,
275
+ writeFileSync: writeFileSyncMock,
276
+ }));
277
+
278
+ jest.mock('@vyriy/script', () => ({
279
+ script: scriptMock,
280
+ }));
281
+
282
+ jest.mock('@p/services/cms', () => ({
283
+ cms: {
284
+ getContent: getContentMock,
285
+ },
286
+ }));
287
+
288
+ describe('workspaces/ssg/index.tsx', () => {
289
+ it('generates a static index HTML file', async () => {
290
+ await import('./index.js');
291
+ await scriptPromise;
292
+
293
+ expect(scriptMock).toHaveBeenCalledTimes(1);
294
+ expect(readFileSyncMock).toHaveBeenCalledWith(expect.stringContaining('styles.css'), 'utf8');
295
+ expect(getContentMock).toHaveBeenCalledTimes(1);
296
+ expect(mkdirSyncMock).toHaveBeenCalledWith(expect.stringContaining('static'), {
297
+ recursive: true,
298
+ });
299
+ expect(writeFileSyncMock).toHaveBeenCalledWith(
300
+ expect.stringContaining('static/index.html'),
301
+ expect.stringContaining('<title>Sample Content</title>'),
302
+ );
303
+
304
+ const generatedHtml = writeFileSyncMock.mock.calls[0]?.[1] as string;
305
+
306
+ expect(generatedHtml).toContain('<style>.content { display: block; }</style>');
307
+ expect(generatedHtml).toContain('This is a sample content fetched from the CMS.');
308
+ });
309
+ });
310
+ `,
5
311
  }),
6
312
  ci: {
7
- github: {
8
- '.gitlab-ci.yml': 'code',
9
- },
10
- },
11
- deploy: {
12
- docker: {
13
- Dockerfile: 'code',
14
- },
313
+ ...base.ci,
15
314
  },
315
+ deploy: {},
16
316
  };
@@ -39,7 +39,7 @@ export const ssr = {
39
39
  'test:jest': 'jest',
40
40
  postinstall: 'husky',
41
41
  },
42
- devDependencies: {
42
+ dependencies: {
43
43
  '@vyriy/typescript-config': `^${packageJson.version}`,
44
44
  typescript: packageJson.peerDependencies.typescript,
45
45
  '@vyriy/prettier-config': `^${packageJson.version}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vyriy",
3
- "version": "0.4.3",
3
+ "version": "0.4.6",
4
4
  "description": "Interactive project master for calm cloud-ready applications.",
5
5
  "type": "module",
6
6
  "bin": "./bin/vyriy.js",
@@ -29,9 +29,6 @@
29
29
  "webpack-cli": "^7.0.2"
30
30
  },
31
31
  "peerDependenciesMeta": {
32
- "@storybook/react-webpack5": {
33
- "optional": true
34
- },
35
32
  "@types/react": {
36
33
  "optional": true
37
34
  },