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.
@@ -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 { presets as appPreset } from '../preset/index.js';
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
  }
@@ -8,7 +8,5 @@ export type PlanResult = {
8
8
  target: string;
9
9
  preset: string;
10
10
  scope?: string;
11
- ci?: string;
12
- deploy?: string;
13
11
  };
14
12
  export type Plan = (dirName: string, appPath: string) => Promise<PlanResult | undefined>;
@@ -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
- files: (options) => ({
5
- ...base.files(options),
6
- ...apiWorkspaceBaseFiles(options.name, options.description),
7
- 'package.json': buildPackageJson(options, [
8
- 'workspaces/*',
9
- ], workspaceScripts('api'), {
10
- ...baseToolingDeps(),
11
- ...webpackDeps(),
12
- ...serverDeps(),
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
- 'workspaces/api/index.test.ts': `import { describe, expect, it, jest } from '@jest/globals';
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
- files: ({ name, description }) => ({
13
- 'package.json': JSON.stringify({
14
- name,
15
- version: '0.0.0',
16
- description,
17
- private: true,
18
- type: 'module',
19
- agents: './AGENTS.md',
20
- packageManager: packageJson.packageManager,
21
- engines: {
22
- node: packageJson.engines.node,
23
- },
24
- scripts: {
25
- storybook: 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook dev -p 6006 --disable-telemetry',
26
- check: 'run-s lint build test',
27
- fix: "run-s 'fix:*'",
28
- lint: "run-s 'lint:*'",
29
- build: "run-s 'build:*'",
30
- test: "run-s 'test:*'",
31
- 'fix:prettier': 'prettier . --write',
32
- 'fix:eslint': 'eslint . --fix',
33
- 'lint:ts': 'tsc',
34
- 'lint:prettier': 'prettier . --check',
35
- 'lint:eslint': 'eslint .',
36
- 'build:storybook': 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook build --quiet --disable-telemetry',
37
- 'test:jest': 'jest --passWithNoTests',
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
- },
56
- }, null, 2) + '\n',
57
- 'README.md': `# ${name}\n\n${description}\n`,
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
- 'AGENTS.md': agentsContent,
66
- '.editorconfig': `# https://editorconfig.org
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
- '.gitignore': `.yarn/*
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
- '.npmrc': 'engine-strict=true\n',
124
- '.nvmrc': 'lts/krypton\n',
125
- '.yarnrc.yml': 'nodeLinker: node-modules\nnpmMinimalAgeGate: 0\n',
126
- '.husky/commit-msg': '#!/bin/sh\n',
127
- '.husky/post-checkout': '#!/bin/sh\n\nyarn\n',
128
- '.husky/post-merge': '#!/bin/sh\n\nyarn\n',
129
- '.husky/pre-commit': '#!/bin/sh\n\nyarn check\n',
130
- '.husky/pre-push': '#!/bin/sh\n\nyarn check\n',
131
- '.storybook/main.ts': `import config from '@vyriy/storybook-config';
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
- '.storybook/preview.tsx': "export { default } from '@vyriy/storybook-config/preview';\n",
143
- 'yarn.lock': '',
144
- 'tsconfig.json': JSON.stringify({
145
- extends: '@vyriy/typescript-config/index.json',
146
- include: [
147
- '.storybook/**/*.ts',
148
- '.storybook/**/*.tsx',
149
- 'packages/**/*.ts',
150
- 'packages/**/*.tsx',
151
- 'workspaces/**/*.ts',
152
- 'workspaces/**/*.tsx',
153
- '*.ts',
154
- ],
155
- }, null, 2) + '\n',
156
- 'prettier.config.ts': "export { default } from '@vyriy/prettier-config';\n",
157
- '.prettierignore': 'node_modules\ndist\ncoverage\nstorybook-static\nconsumer\n',
158
- 'eslint.config.ts': "export { default } from '@vyriy/eslint-config';\n",
159
- 'jest.config.ts': "export { default } from '@vyriy/jest-config';\n",
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,2 @@
1
+ import type { Preset } from './types.js';
2
+ export declare const fullstack: Preset;
@@ -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
+ });