vyriy 0.5.1 → 0.5.2
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/commands/create/index.js +1 -6
- package/commands/create/plan/plan.d.ts +1 -3
- package/commands/create/plan/plan.js +8 -7
- package/commands/create/plan/types.d.ts +0 -2
- package/commands/create/preset/api.js +13 -19
- package/commands/create/preset/base.js +78 -114
- package/commands/create/preset/fullstack.d.ts +2 -0
- package/commands/create/preset/fullstack.js +158 -0
- package/commands/create/preset/gql.js +82 -88
- package/commands/create/preset/index.d.ts +5 -0
- package/commands/create/preset/index.js +6 -0
- package/commands/create/preset/library.js +115 -121
- package/commands/create/preset/mfe.js +97 -104
- package/commands/create/preset/rest.js +15 -21
- package/commands/create/preset/spa.js +31 -37
- package/commands/create/preset/ssg.js +32 -38
- package/commands/create/preset/ssr.js +32 -38
- package/commands/create/preset/types.d.ts +2 -8
- package/commands/create/prompt/index.d.ts +0 -1
- package/commands/create/prompt/index.js +0 -1
- package/package.json +1 -1
- package/commands/create/prompt/provider.d.ts +0 -2
- package/commands/create/prompt/provider.js +0 -13
package/commands/create/index.js
CHANGED
|
@@ -8,18 +8,13 @@ import { plan } from './plan/index.js';
|
|
|
8
8
|
import { conflictStrategy as promptConflictStrategy } from './prompt/index.js';
|
|
9
9
|
import { presets } from './preset/index.js';
|
|
10
10
|
const exec = promisify(processExec);
|
|
11
|
-
const getProviderFiles = (providers, provider) => provider === undefined ? {} : (providers[provider] ?? {});
|
|
12
11
|
const isPresetName = (preset) => preset in presets;
|
|
13
12
|
const mergeFiles = (planOption) => {
|
|
14
13
|
if (!isPresetName(planOption.preset)) {
|
|
15
14
|
return undefined;
|
|
16
15
|
}
|
|
17
16
|
const preset = presets[planOption.preset].preset;
|
|
18
|
-
return
|
|
19
|
-
...preset.files(planOption),
|
|
20
|
-
...getProviderFiles(preset.ci, planOption.ci),
|
|
21
|
-
...getProviderFiles(preset.deploy, planOption.deploy),
|
|
22
|
-
};
|
|
17
|
+
return preset(planOption);
|
|
23
18
|
};
|
|
24
19
|
const getSortedFileNames = (files) => Object.keys(files).sort((a, b) => a.localeCompare(b));
|
|
25
20
|
const logFilePlan = (target, files) => {
|
|
@@ -2,8 +2,6 @@ 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" | "gql" | "ssg" | "spa" | "mfe";
|
|
5
|
+
preset: "ssr" | "base" | "rest" | "api" | "library" | "gql" | "ssg" | "spa" | "mfe" | "fullstack";
|
|
6
6
|
scope: string | undefined;
|
|
7
|
-
ci: import("../preset/types.js").CiProvider | undefined;
|
|
8
|
-
deploy: import("../preset/types.js").DeployProvider | undefined;
|
|
9
7
|
} | undefined>;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { stdin, stdout } from 'node:process';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
2
3
|
import { createInterface } from 'node:readline';
|
|
3
|
-
import {
|
|
4
|
-
import { prompt, provider as promptProvider, preset as promptPreset, scope as promptScope } from '../prompt/index.js';
|
|
4
|
+
import { prompt, preset as promptPreset, scope as promptScope } from '../prompt/index.js';
|
|
5
5
|
import { question as createQuestion } from './question.js';
|
|
6
|
+
const toDirectoryName = (name, fallback) => {
|
|
7
|
+
const directoryName = name.trim().replaceAll(/[\s\\/]+/g, '_');
|
|
8
|
+
return directoryName || fallback;
|
|
9
|
+
};
|
|
10
|
+
const getTargetDefault = (name, dirName, appPath) => name === dirName ? appPath : resolve(dirname(appPath), toDirectoryName(name, dirName));
|
|
6
11
|
export const plan = async (dirName, appPath) => {
|
|
7
12
|
const readline = createInterface({ input: stdin, output: stdout });
|
|
8
13
|
const question = createQuestion(readline, stdout);
|
|
@@ -10,11 +15,9 @@ export const plan = async (dirName, appPath) => {
|
|
|
10
15
|
stdout.write('\nVyriy Project Master\n\n');
|
|
11
16
|
const name = await prompt(question, 'Project name', dirName);
|
|
12
17
|
const description = await prompt(question, 'Project description', 'Calm cloud-ready application');
|
|
13
|
-
const target = await prompt(question, 'Target directory', appPath);
|
|
18
|
+
const target = await prompt(question, 'Target directory', getTargetDefault(name, dirName, appPath));
|
|
14
19
|
const preset = await promptPreset(question, stdout);
|
|
15
20
|
const scope = await promptScope(question, preset, name);
|
|
16
|
-
const ci = await promptProvider(question, stdout, 'CI/CD provider', appPreset[preset].preset.ci);
|
|
17
|
-
const deploy = await promptProvider(question, stdout, 'Deploy provider', appPreset[preset].preset.deploy);
|
|
18
21
|
const confirmation = (await prompt(question, 'Use this project plan?', 'y')).toLowerCase();
|
|
19
22
|
return confirmation === 'y'
|
|
20
23
|
? {
|
|
@@ -23,8 +26,6 @@ export const plan = async (dirName, appPath) => {
|
|
|
23
26
|
target,
|
|
24
27
|
preset,
|
|
25
28
|
scope,
|
|
26
|
-
ci,
|
|
27
|
-
deploy,
|
|
28
29
|
}
|
|
29
30
|
: undefined;
|
|
30
31
|
}
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { base } from './base.js';
|
|
2
2
|
import { apiWorkspaceBaseFiles, baseToolingDeps, buildPackageJson, serverDeps, webpackDeps, workspaceScripts, } from './shared.js';
|
|
3
|
-
export const api = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
'workspaces/api/index.ts': `import { server } from '@vyriy/server';
|
|
3
|
+
export const api = (options) => ({
|
|
4
|
+
...base(options),
|
|
5
|
+
...apiWorkspaceBaseFiles(options.name, options.description),
|
|
6
|
+
'package.json': buildPackageJson(options, [
|
|
7
|
+
'workspaces/*',
|
|
8
|
+
], workspaceScripts('api'), {
|
|
9
|
+
...baseToolingDeps(),
|
|
10
|
+
...webpackDeps(),
|
|
11
|
+
...serverDeps(),
|
|
12
|
+
}),
|
|
13
|
+
'workspaces/api/index.ts': `import { server } from '@vyriy/server';
|
|
15
14
|
import { api } from '@vyriy/handler';
|
|
16
15
|
|
|
17
16
|
server(
|
|
@@ -25,7 +24,7 @@ server(
|
|
|
25
24
|
),
|
|
26
25
|
);
|
|
27
26
|
`,
|
|
28
|
-
|
|
27
|
+
'workspaces/api/index.test.ts': `import { describe, expect, it, jest } from '@jest/globals';
|
|
29
28
|
|
|
30
29
|
const apiMock = jest.fn((handler) => ({
|
|
31
30
|
handler,
|
|
@@ -61,9 +60,4 @@ describe('workspaces/api/index.ts', () => {
|
|
|
61
60
|
});
|
|
62
61
|
});
|
|
63
62
|
`,
|
|
64
|
-
|
|
65
|
-
ci: {
|
|
66
|
-
...base.ci,
|
|
67
|
-
},
|
|
68
|
-
deploy: {},
|
|
69
|
-
};
|
|
63
|
+
});
|
|
@@ -8,62 +8,61 @@ const agentsPath = [
|
|
|
8
8
|
resolve(presetDir, '../../../../AGENTS.md'),
|
|
9
9
|
].find(existsSync) ?? '';
|
|
10
10
|
const agentsContent = agentsPath ? readFileSync(agentsPath, 'utf8') : '';
|
|
11
|
-
export const base = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
'doc.mdx': `import { Meta, Markdown } from '@storybook/addon-docs/blocks';
|
|
11
|
+
export const base = ({ name, description }) => ({
|
|
12
|
+
'package.json': JSON.stringify({
|
|
13
|
+
name,
|
|
14
|
+
version: '0.0.0',
|
|
15
|
+
description,
|
|
16
|
+
private: true,
|
|
17
|
+
type: 'module',
|
|
18
|
+
agents: './AGENTS.md',
|
|
19
|
+
packageManager: packageJson.packageManager,
|
|
20
|
+
engines: {
|
|
21
|
+
node: packageJson.engines.node,
|
|
22
|
+
},
|
|
23
|
+
scripts: {
|
|
24
|
+
storybook: 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook dev -p 6006 --disable-telemetry',
|
|
25
|
+
check: 'run-s lint build test',
|
|
26
|
+
fix: "run-s 'fix:*'",
|
|
27
|
+
lint: "run-s 'lint:*'",
|
|
28
|
+
build: "run-s 'build:*'",
|
|
29
|
+
test: "run-s 'test:*'",
|
|
30
|
+
'fix:prettier': 'prettier . --write',
|
|
31
|
+
'fix:eslint': 'eslint . --fix',
|
|
32
|
+
'lint:ts': 'tsc',
|
|
33
|
+
'lint:prettier': 'prettier . --check',
|
|
34
|
+
'lint:eslint': 'eslint .',
|
|
35
|
+
'build:storybook': 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook build --quiet --disable-telemetry',
|
|
36
|
+
'test:jest': 'jest --passWithNoTests',
|
|
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
|
+
husky: packageJson.peerDependencies.husky,
|
|
52
|
+
'npm-run-all2': packageJson.peerDependencies['npm-run-all2'],
|
|
53
|
+
'cross-env': packageJson.peerDependencies['cross-env'],
|
|
54
|
+
},
|
|
55
|
+
}, null, 2) + '\n',
|
|
56
|
+
'README.md': `# ${name}\n\n${description}\n`,
|
|
57
|
+
'doc.mdx': `import { Meta, Markdown } from '@storybook/addon-docs/blocks';
|
|
59
58
|
import ReadMe from './README.md?raw';
|
|
60
59
|
|
|
61
60
|
<Meta title="${name}" />
|
|
62
61
|
|
|
63
62
|
<Markdown>{ReadMe}</Markdown>
|
|
64
63
|
`,
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
'AGENTS.md': agentsContent,
|
|
65
|
+
'.editorconfig': `# https://editorconfig.org
|
|
67
66
|
root = true
|
|
68
67
|
|
|
69
68
|
[*]
|
|
@@ -98,7 +97,7 @@ indent_size = 2
|
|
|
98
97
|
[*.sh]
|
|
99
98
|
indent_size = 2
|
|
100
99
|
`,
|
|
101
|
-
|
|
100
|
+
'.gitignore': `.yarn/*
|
|
102
101
|
!.yarn/cache
|
|
103
102
|
!.yarn/patches
|
|
104
103
|
!.yarn/plugins
|
|
@@ -120,15 +119,15 @@ cdk.context.json
|
|
|
120
119
|
|
|
121
120
|
!/**/.gitkeep
|
|
122
121
|
`,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
122
|
+
'.npmrc': 'engine-strict=true\n',
|
|
123
|
+
'.nvmrc': 'lts/krypton\n',
|
|
124
|
+
'.yarnrc.yml': 'nodeLinker: node-modules\nnpmMinimalAgeGate: 0\n',
|
|
125
|
+
'.husky/commit-msg': '#!/bin/sh\n',
|
|
126
|
+
'.husky/post-checkout': '#!/bin/sh\n\nyarn\n',
|
|
127
|
+
'.husky/post-merge': '#!/bin/sh\n\nyarn\n',
|
|
128
|
+
'.husky/pre-commit': '#!/bin/sh\n\nyarn check\n',
|
|
129
|
+
'.husky/pre-push': '#!/bin/sh\n\nyarn check\n',
|
|
130
|
+
'.storybook/main.ts': `import config from '@vyriy/storybook-config';
|
|
132
131
|
import { path } from '@vyriy/path';
|
|
133
132
|
|
|
134
133
|
export default {
|
|
@@ -139,57 +138,22 @@ export default {
|
|
|
139
138
|
],
|
|
140
139
|
};
|
|
141
140
|
`,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
ci: {
|
|
162
|
-
gitlab: {
|
|
163
|
-
'.gitlab-ci.yml': `image: node:24
|
|
164
|
-
|
|
165
|
-
code:
|
|
166
|
-
script:
|
|
167
|
-
- corepack enable
|
|
168
|
-
- yarn install
|
|
169
|
-
- yarn check
|
|
170
|
-
`,
|
|
171
|
-
},
|
|
172
|
-
github: {
|
|
173
|
-
'.github/workflows/code.yml': `name: code
|
|
174
|
-
|
|
175
|
-
on:
|
|
176
|
-
push:
|
|
177
|
-
pull_request:
|
|
178
|
-
|
|
179
|
-
jobs:
|
|
180
|
-
code:
|
|
181
|
-
runs-on: ubuntu-latest
|
|
182
|
-
steps:
|
|
183
|
-
- uses: actions/checkout@v4
|
|
184
|
-
- uses: actions/setup-node@v4
|
|
185
|
-
with:
|
|
186
|
-
node-version: 24
|
|
187
|
-
- run: |
|
|
188
|
-
corepack enable
|
|
189
|
-
yarn install
|
|
190
|
-
yarn check
|
|
191
|
-
`,
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
deploy: {},
|
|
195
|
-
};
|
|
141
|
+
'.storybook/preview.tsx': "export { default } from '@vyriy/storybook-config/preview';\n",
|
|
142
|
+
'yarn.lock': '',
|
|
143
|
+
'tsconfig.json': JSON.stringify({
|
|
144
|
+
extends: '@vyriy/typescript-config/index.json',
|
|
145
|
+
include: [
|
|
146
|
+
'.storybook/**/*.ts',
|
|
147
|
+
'.storybook/**/*.tsx',
|
|
148
|
+
'packages/**/*.ts',
|
|
149
|
+
'packages/**/*.tsx',
|
|
150
|
+
'workspaces/**/*.ts',
|
|
151
|
+
'workspaces/**/*.tsx',
|
|
152
|
+
'*.ts',
|
|
153
|
+
],
|
|
154
|
+
}, null, 2) + '\n',
|
|
155
|
+
'prettier.config.ts': "export { default } from '@vyriy/prettier-config';\n",
|
|
156
|
+
'.prettierignore': 'node_modules\ndist\ncoverage\nstorybook-static\nconsumer\n',
|
|
157
|
+
'eslint.config.ts': "export { default } from '@vyriy/eslint-config';\n",
|
|
158
|
+
'jest.config.ts': "export { default } from '@vyriy/jest-config';\n",
|
|
159
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import packageJson from '../../../package.json' with { type: 'json' };
|
|
2
|
+
import { mfe } from './mfe.js';
|
|
3
|
+
const mfeOnlyPaths = [
|
|
4
|
+
'packages/api/',
|
|
5
|
+
'packages/event/',
|
|
6
|
+
'packages/query/',
|
|
7
|
+
'workspaces/api/index.ts',
|
|
8
|
+
'workspaces/api/index.test.ts',
|
|
9
|
+
'workspaces/static/public/icon.svg',
|
|
10
|
+
'workspaces/static/public/screenshots/',
|
|
11
|
+
];
|
|
12
|
+
const getSharedFiles = (options) => Object.fromEntries(Object.entries(mfe(options)).filter(([file]) => !mfeOnlyPaths.some((path) => file.startsWith(path))));
|
|
13
|
+
const projectFiles = {
|
|
14
|
+
'.browserslistrc': '[development]\nextends @vyriy/browserslist-config\n\n[ssr]\nextends @vyriy/browserslist-config\n\n[production]\nextends @vyriy/browserslist-config\n\n[modern]\nextends @vyriy/browserslist-config\n',
|
|
15
|
+
'.storybook/preview.tsx': "import '../packages/components/styles.scss';\n\nexport { default } from '@vyriy/storybook-config/preview';\n",
|
|
16
|
+
'packages/env/env.test.ts': "import { afterEach, describe, expect, it } from '@jest/globals';\n\nimport { getApi, getCdn, getUi } from './env.js';\n\ndescribe('env getters', () => {\n afterEach(() => {\n delete process.env.API;\n delete process.env.CDN;\n delete process.env.UI;\n });\n\n it('reads required environment values', () => {\n process.env.API = 'http://localhost:3000';\n process.env.CDN = 'http://localhost:3001';\n process.env.UI = 'http://localhost:3002';\n\n expect(getApi()).toBe('http://localhost:3000');\n expect(getCdn()).toBe('http://localhost:3001');\n expect(getUi()).toBe('http://localhost:3002');\n });\n\n it('throws when a required environment value is missing', () => {\n expect(() => getUi()).toThrow('Environment variable UI is not defined!');\n });\n});\n",
|
|
17
|
+
'packages/env/env.ts': "import { getEnv } from '@vyriy/env';\n\n/** Reads the API origin used for server endpoints. */\nexport const getApi = () => getEnv('API');\n\n/** Reads the CDN origin used for static assets. */\nexport const getCdn = () => getEnv('CDN');\n\n/** Reads the UI origin used for browser assets. */\nexport const getUi = () => getEnv('UI');\n",
|
|
18
|
+
'packages/env/index.test.ts': "import { afterEach, describe, expect, it } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\nconst ENV_NAMES = [\n 'API',\n 'CDN',\n 'UI',\n] as const;\n\nconst clearEnv = () => {\n for (const name of ENV_NAMES) {\n delete process.env[name];\n }\n};\n\ndescribe('env public API', () => {\n afterEach(() => {\n clearEnv();\n });\n\n it('exports env getters', () => {\n expect(publicApi.getApi).toBeDefined();\n expect(publicApi.getCdn).toBeDefined();\n expect(publicApi.getUi).toBeDefined();\n });\n\n it('reads environment variables by public getter name', () => {\n process.env.API = 'http://localhost:3000';\n process.env.CDN = 'http://localhost:3001';\n process.env.UI = 'http://localhost:3002';\n\n expect(publicApi.getApi()).toBe('http://localhost:3000');\n expect(publicApi.getCdn()).toBe('http://localhost:3001');\n expect(publicApi.getUi()).toBe('http://localhost:3002');\n });\n\n it('throws when a required environment variable is missing', () => {\n clearEnv();\n\n expect(() => publicApi.getApi()).toThrow('Environment variable API is not defined!');\n });\n});\n",
|
|
19
|
+
'packages/env/README.md': '# @p/env\n\nRequired environment readers shared by API and UI workspaces.\n\n## Exports\n\n- `getApi()` reads `API`.\n- `getCdn()` reads `CDN`.\n- `getUi()` reads `UI`.\n\nEach getter throws when its environment variable is missing.\n',
|
|
20
|
+
'workspaces/api/index.test.tsx': "import { describe, expect, it, jest } from '@jest/globals';\nimport type { APIGatewayProxyEvent } from '@vyriy/router';\n\nconst apiMock = jest.fn((handler) => ({ handler }));\nconst serverMock = jest.fn();\n\njest.mock('@vyriy/handler', () => ({\n api: apiMock,\n}));\n\njest.mock('@vyriy/server', () => ({\n server: serverMock,\n}));\n\njest.mock('@p/env', () => ({\n getUi: () => 'http://localhost:3002',\n}));\n\ndescribe('workspaces/api/index.tsx', () => {\n type ApiHandler = (event: APIGatewayProxyEvent) => Promise<{\n body: string;\n headers?: Record<string, string>;\n statusCode: number;\n }>;\n\n const getEvent = (path: string): APIGatewayProxyEvent =>\n ({\n body: null,\n headers: {},\n httpMethod: 'GET',\n path,\n pathParameters: null,\n queryStringParameters: null,\n }) as APIGatewayProxyEvent;\n\n const loadHandler = async (): Promise<ApiHandler> => {\n await jest.isolateModulesAsync(async () => {\n await import('./index.js');\n });\n\n expect(apiMock).toHaveBeenCalledTimes(1);\n expect(serverMock).toHaveBeenCalledTimes(1);\n expect(serverMock).toHaveBeenCalledWith(apiMock.mock.results[0]?.value);\n\n return apiMock.mock.calls[0]?.[0] as ApiHandler;\n };\n\n it('starts the server with the API handler', async () => {\n await loadHandler();\n\n expect(apiMock).toHaveBeenCalledTimes(1);\n });\n\n it('renders the demo page for the root route', async () => {\n const handler = await loadHandler();\n const response = await handler(getEvent('/'));\n\n expect(response).toEqual({\n body: expect.any(String),\n headers: {\n 'access-control-allow-origin': '*',\n 'cache-control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400',\n 'content-type': 'text/html; charset=utf-8',\n 'x-content-type-options': 'nosniff',\n },\n isBase64Encoded: undefined,\n multiValueHeaders: undefined,\n statusCode: 200,\n });\n expect(response.body).toContain('<title>Demo</title>');\n expect(response.body).toContain('href=\"http://localhost:3002/main.css\"');\n expect(response.body).toContain('<div id=\"root\" rendered>');\n expect(response.body).toContain('Developer');\n expect(response.body).toContain('Senior IT Professional');\n expect(response.body).toContain('http://localhost:3001/avatar.svg');\n expect(response.body).toContain('src=\"http://localhost:3002/index.js\"');\n });\n\n it('returns not found for unknown routes', async () => {\n const handler = await loadHandler();\n\n await expect(handler(getEvent('/missing'))).resolves.toEqual({\n body: JSON.stringify({\n message: 'Not Found',\n }),\n statusCode: 404,\n });\n });\n});\n",
|
|
21
|
+
'workspaces/api/index.tsx': "import { server } from '@vyriy/server';\nimport { api } from '@vyriy/handler';\nimport { createRouter } from '@vyriy/router';\nimport { minify, html } from '@vyriy/html';\nimport { html as react } from '@vyriy/render';\n\nimport { ProfileCard } from '@p/components/profile-card';\nimport { getUi } from '@p/env';\n\nserver(\n api(async (event) =>\n createRouter()\n .get('/', () => ({\n body: minify(\n html({\n htmlAttributes: 'lang=\"en\"',\n title: '<title>Demo</title>',\n meta: '<meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />',\n link: `<link rel=\"stylesheet\" type=\"text/css\" href=\"${getUi()}/main.css\" />`,\n body: `<div id=\"root\" rendered>${react(\n <ProfileCard\n name=\"Developer\"\n title=\"Senior IT Professional\"\n avatarUrl=\"http://localhost:3001/avatar.svg\"\n />,\n )}</div>`,\n script: `<script defer=\"defer\" src=\"${getUi()}/index.js\"></script>`,\n }),\n ),\n headers: {\n 'content-type': 'text/html; charset=utf-8',\n 'cache-control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400',\n 'access-control-allow-origin': '*',\n 'x-content-type-options': 'nosniff',\n },\n }))\n .route(event),\n ),\n);\n",
|
|
22
|
+
'workspaces/api/webpack.config.ts': "import { EnvironmentPlugin } from 'webpack';\nimport { path } from '@vyriy/path';\nimport { ssr, external } from '@vyriy/webpack-config';\n\nexport default ssr(\n '@w/api',\n {\n path: path('dist', 'api'),\n filename: 'index.js',\n library: { type: 'commonjs2' },\n },\n (config) => ({\n ...config,\n externals: [external({ allowlist: [/^@p/, /^@w/, /^@vyriy/] })],\n plugins: [\n ...(config.plugins ?? []),\n new EnvironmentPlugin([\n 'API',\n 'CDN',\n 'UI',\n ]),\n ],\n }),\n);\n",
|
|
23
|
+
'workspaces/env.sh': '#!/usr/bin/env sh\n\n: "${API_PORT:=3000}"\n: "${CDN_PORT:=3001}"\n: "${UI_PORT:=3002}"\n: "${API:=http://localhost:$API_PORT}"\n: "${CDN:=http://localhost:$CDN_PORT}"\n: "${UI:=http://localhost:$UI_PORT}"\n\nexport API_PORT\nexport CDN_PORT\nexport UI_PORT\nexport API\nexport CDN\nexport UI\n',
|
|
24
|
+
'workspaces/static/README.md': '# @w/static\n\nStatic asset workspace for the profile-card UI.\n\n## Assets\n\n- `avatar.svg` is the default demo avatar.\n\nThe workspace is served as the CDN origin during local development.\n',
|
|
25
|
+
'workspaces/ui/index.test.tsx': "import { describe, expect, it, jest } from '@jest/globals';\nimport { isValidElement } from 'react';\nimport type { ReactElement } from 'react';\n\nconst elementMock = jest.fn();\n\njest.mock('@vyriy/render/element', () => ({\n element: elementMock,\n}));\n\ntype ProfileCardProps = {\n avatarUrl: string;\n name: string;\n title: string;\n};\n\ndescribe('workspaces/ui/index.tsx', () => {\n const loadEntry = async () => {\n const root = document.createElement('div');\n root.id = 'root';\n document.body.replaceChildren();\n document.body.append(root);\n\n await jest.isolateModulesAsync(async () => {\n await import('./index.js');\n });\n\n const [{ component }] = elementMock.mock.calls[0] as [{ component: ReactElement<ProfileCardProps> }];\n\n return {\n root,\n component,\n };\n };\n\n it('mounts the UI into the root element', async () => {\n const { root, component } = await loadEntry();\n\n expect(elementMock).toHaveBeenCalledTimes(1);\n expect(elementMock).toHaveBeenCalledWith({\n root,\n component,\n });\n });\n\n it('renders the profile card demo component', async () => {\n const { component } = await loadEntry();\n\n expect(isValidElement(component)).toBe(true);\n expect(typeof component.type).toBe('function');\n expect((component.type as { name?: string }).name).toBe('ProfileCard');\n expect(component.props).toEqual({\n avatarUrl: 'http://localhost:3001/avatar.svg',\n name: 'Developer',\n title: 'Senior IT Professional',\n });\n });\n});\n",
|
|
26
|
+
'workspaces/ui/index.tsx': "import { element } from '@vyriy/render/element';\n\nimport { ProfileCard } from '@p/components/profile-card';\nimport '@p/components/styles.scss';\n\nelement({\n root: document.getElementById('root'),\n component: (\n <ProfileCard name=\"Developer\" title=\"Senior IT Professional\" avatarUrl=\"http://localhost:3001/avatar.svg\" />\n ),\n});\n",
|
|
27
|
+
'workspaces/ui/webpack.config.ts': "import { EnvironmentPlugin } from 'webpack';\n\nimport { csr, html } from '@vyriy/webpack-config';\nimport { path } from '@vyriy/path';\n\nexport default csr(\n '@w/ui',\n {\n path: path('dist', 'cdn'),\n filename: 'index.js',\n },\n (config) => ({\n ...config,\n plugins: [\n ...(config.plugins ?? []),\n new EnvironmentPlugin(['API', 'CDN', 'UI']),\n html({\n htmlAttributes: 'lang=\"en\"',\n title: '<title>Demo</title>',\n meta: '<meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />',\n body: '<div id=\"root\"></div>',\n }),\n ],\n }),\n);\n",
|
|
28
|
+
};
|
|
29
|
+
export const fullstack = (options) => ({
|
|
30
|
+
...getSharedFiles(options),
|
|
31
|
+
...projectFiles,
|
|
32
|
+
'package.json': JSON.stringify({
|
|
33
|
+
name: options.name,
|
|
34
|
+
version: '0.0.0',
|
|
35
|
+
description: options.description,
|
|
36
|
+
private: true,
|
|
37
|
+
type: 'module',
|
|
38
|
+
agents: './AGENTS.md',
|
|
39
|
+
packageManager: packageJson.packageManager,
|
|
40
|
+
engines: {
|
|
41
|
+
node: packageJson.engines.node,
|
|
42
|
+
},
|
|
43
|
+
workspaces: [
|
|
44
|
+
'packages/*',
|
|
45
|
+
'workspaces/*',
|
|
46
|
+
],
|
|
47
|
+
scripts: {
|
|
48
|
+
storybook: 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook dev -p 6006 --disable-telemetry',
|
|
49
|
+
check: 'run-s lint build test',
|
|
50
|
+
fix: "run-s 'fix:*'",
|
|
51
|
+
start: "run-p 'start:*'",
|
|
52
|
+
lint: "run-s 'lint:*'",
|
|
53
|
+
build: "run-s 'build:*'",
|
|
54
|
+
test: "run-s 'test:*'",
|
|
55
|
+
'fix:prettier': 'prettier . --write',
|
|
56
|
+
'fix:eslint': 'eslint . --fix',
|
|
57
|
+
'fix:stylelint': "stylelint '**/*.{css,scss}' --fix",
|
|
58
|
+
'start:api': 'sh workspaces/api/bin/start.sh',
|
|
59
|
+
'start:static': 'sh workspaces/static/bin/start.sh',
|
|
60
|
+
'start:ui': 'sh workspaces/ui/bin/start.sh',
|
|
61
|
+
'lint:ts': 'tsc',
|
|
62
|
+
'lint:prettier': 'prettier . --check',
|
|
63
|
+
'lint:eslint': 'eslint .',
|
|
64
|
+
'lint:stylelint': "stylelint '**/*.{css,scss}'",
|
|
65
|
+
'build:api': 'sh workspaces/api/bin/build.sh',
|
|
66
|
+
'build:ui': 'sh workspaces/ui/bin/build.sh',
|
|
67
|
+
'build:static': 'sh workspaces/static/bin/build.sh',
|
|
68
|
+
'build:storybook': 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook build --quiet --disable-telemetry',
|
|
69
|
+
'test:jest': 'jest',
|
|
70
|
+
prebuild: 'rimraf dist',
|
|
71
|
+
postinstall: 'husky',
|
|
72
|
+
},
|
|
73
|
+
dependencies: {
|
|
74
|
+
'@testing-library/dom': packageJson.peerDependencies['@testing-library/dom'],
|
|
75
|
+
'@testing-library/react': packageJson.peerDependencies['@testing-library/react'],
|
|
76
|
+
'@types/jest': packageJson.peerDependencies['@types/jest'],
|
|
77
|
+
'@vyriy/browserslist-config': `^${packageJson.version}`,
|
|
78
|
+
'@vyriy/cn': `^${packageJson.version}`,
|
|
79
|
+
'@vyriy/env': `^${packageJson.version}`,
|
|
80
|
+
'@vyriy/eslint-config': `^${packageJson.version}`,
|
|
81
|
+
'@vyriy/handler': `^${packageJson.version}`,
|
|
82
|
+
'@vyriy/html': `^${packageJson.version}`,
|
|
83
|
+
'@vyriy/jest-config': `^${packageJson.version}`,
|
|
84
|
+
'@vyriy/path': `^${packageJson.version}`,
|
|
85
|
+
'@vyriy/prettier-config': `^${packageJson.version}`,
|
|
86
|
+
'@vyriy/render': `^${packageJson.version}`,
|
|
87
|
+
'@vyriy/router': `^${packageJson.version}`,
|
|
88
|
+
'@vyriy/server': `^${packageJson.version}`,
|
|
89
|
+
'@vyriy/storybook-config': `^${packageJson.version}`,
|
|
90
|
+
'@vyriy/stylelint-config': `^${packageJson.version}`,
|
|
91
|
+
'@vyriy/typescript-config': `^${packageJson.version}`,
|
|
92
|
+
'@vyriy/webpack-config': `^${packageJson.version}`,
|
|
93
|
+
'cross-env': packageJson.peerDependencies['cross-env'],
|
|
94
|
+
eslint: packageJson.peerDependencies.eslint,
|
|
95
|
+
husky: packageJson.peerDependencies.husky,
|
|
96
|
+
jest: packageJson.peerDependencies.jest,
|
|
97
|
+
'npm-run-all2': packageJson.peerDependencies['npm-run-all2'],
|
|
98
|
+
prettier: packageJson.peerDependencies.prettier,
|
|
99
|
+
rimraf: packageJson.peerDependencies.rimraf,
|
|
100
|
+
serve: packageJson.peerDependencies.serve,
|
|
101
|
+
storybook: packageJson.peerDependencies.storybook,
|
|
102
|
+
stylelint: packageJson.peerDependencies.stylelint,
|
|
103
|
+
tsx: packageJson.peerDependencies.tsx,
|
|
104
|
+
typescript: packageJson.peerDependencies.typescript,
|
|
105
|
+
webpack: packageJson.peerDependencies.webpack,
|
|
106
|
+
'webpack-cli': packageJson.peerDependencies['webpack-cli'],
|
|
107
|
+
},
|
|
108
|
+
}, null, 2) + '\n',
|
|
109
|
+
'README.md': `# ${options.name}
|
|
110
|
+
|
|
111
|
+
${options.description}
|
|
112
|
+
|
|
113
|
+
## Setup
|
|
114
|
+
|
|
115
|
+
\`\`\`bash
|
|
116
|
+
yarn install
|
|
117
|
+
\`\`\`
|
|
118
|
+
|
|
119
|
+
## Start
|
|
120
|
+
|
|
121
|
+
Start the API, static asset server, and UI dev server together:
|
|
122
|
+
|
|
123
|
+
\`\`\`bash
|
|
124
|
+
yarn start
|
|
125
|
+
\`\`\`
|
|
126
|
+
|
|
127
|
+
Start individual workspaces:
|
|
128
|
+
|
|
129
|
+
\`\`\`bash
|
|
130
|
+
yarn start:api
|
|
131
|
+
yarn start:static
|
|
132
|
+
yarn start:ui
|
|
133
|
+
\`\`\`
|
|
134
|
+
|
|
135
|
+
## Local URLs
|
|
136
|
+
|
|
137
|
+
Default ports are defined in \`workspaces/env.sh\`:
|
|
138
|
+
|
|
139
|
+
- API: \`http://localhost:3000\`
|
|
140
|
+
- Static/CDN assets: \`http://localhost:3001\`
|
|
141
|
+
- UI dev server: \`http://localhost:3002\`
|
|
142
|
+
|
|
143
|
+
## Validation
|
|
144
|
+
|
|
145
|
+
\`\`\`bash
|
|
146
|
+
yarn lint
|
|
147
|
+
yarn test
|
|
148
|
+
yarn build
|
|
149
|
+
\`\`\`
|
|
150
|
+
`,
|
|
151
|
+
'doc.mdx': `import { Meta, Markdown } from '@storybook/addon-docs/blocks';
|
|
152
|
+
import ReadMe from './README.md?raw';
|
|
153
|
+
|
|
154
|
+
<Meta title="${options.name}" />
|
|
155
|
+
|
|
156
|
+
<Markdown>{ReadMe}</Markdown>
|
|
157
|
+
`,
|
|
158
|
+
});
|