stack-service-base 0.0.99 → 0.0.100

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/stack-service-base/command_init.rb +5 -3
  3. data/lib/stack-service-base/frontend_template/github/.github/workflows/main.yml +19 -0
  4. data/lib/stack-service-base/frontend_template/github/docker/docker-compose.yml +15 -0
  5. data/lib/stack-service-base/frontend_template/gitlab/.gitlab-ci.yml +48 -0
  6. data/lib/stack-service-base/frontend_template/gitlab/docker/Dockerfile.build +36 -0
  7. data/lib/stack-service-base/frontend_template/gitlab/docker/docker-compose.yml +15 -0
  8. data/lib/stack-service-base/frontend_template/gitlab/docker/local_build.sh +10 -0
  9. data/lib/stack-service-base/frontend_template/gitlab-c/.gitlab-ci.yml +24 -0
  10. data/lib/stack-service-base/frontend_template/gitlab-c/docker/Dockerfile.build +17 -0
  11. data/lib/stack-service-base/frontend_template/gitlab-c/docker/docker-compose.yml +15 -0
  12. data/lib/stack-service-base/frontend_template/gitlab-c/docker/local_build.sh +11 -0
  13. data/lib/stack-service-base/frontend_template/home/.editorconfig +12 -0
  14. data/lib/stack-service-base/frontend_template/home/.gitattributes +4 -0
  15. data/lib/stack-service-base/frontend_template/home/.gitignore +17 -0
  16. data/lib/stack-service-base/frontend_template/home/AGENTS.md +3 -0
  17. data/lib/stack-service-base/frontend_template/home/README.md +68 -0
  18. data/lib/stack-service-base/frontend_template/home/docker/.env +3 -0
  19. data/lib/stack-service-base/frontend_template/home/docker/frontend/15-runtime-settings.envsh +90 -0
  20. data/lib/stack-service-base/frontend_template/home/docker/frontend/40-path-prefix-settings.sh +30 -0
  21. data/lib/stack-service-base/frontend_template/home/docker/frontend/45-runtime-config-settings.sh +24 -0
  22. data/lib/stack-service-base/frontend_template/home/docker/frontend/Dockerfile +46 -0
  23. data/lib/stack-service-base/frontend_template/home/docker/frontend/nginx.conf.template +75 -0
  24. data/lib/stack-service-base/frontend_template/home/src/.dockerignore +9 -0
  25. data/lib/stack-service-base/frontend_template/home/src/.env.example +7 -0
  26. data/lib/stack-service-base/frontend_template/home/src/.version +1 -0
  27. data/lib/stack-service-base/frontend_template/home/src/app/main.ts +4 -0
  28. data/lib/stack-service-base/frontend_template/home/src/app/runtime-config.ts +22 -0
  29. data/lib/stack-service-base/frontend_template/home/src/app/runtime-config.types.ts +5 -0
  30. data/lib/stack-service-base/frontend_template/home/src/app/style.css +82 -0
  31. data/lib/stack-service-base/frontend_template/home/src/app/vite-env.d.ts +1 -0
  32. data/lib/stack-service-base/frontend_template/home/src/eslint.config.js +34 -0
  33. data/lib/stack-service-base/frontend_template/home/src/index.html +24 -0
  34. data/lib/stack-service-base/frontend_template/home/src/package-lock.json +2812 -0
  35. data/lib/stack-service-base/frontend_template/home/src/package.json +30 -0
  36. data/lib/stack-service-base/frontend_template/home/src/proxy.config.json +8 -0
  37. data/lib/stack-service-base/frontend_template/home/src/public/runtime-config.js +1 -0
  38. data/lib/stack-service-base/frontend_template/home/src/tests/build-proxy.test.ts +28 -0
  39. data/lib/stack-service-base/frontend_template/home/src/tests/javascript-support.test.js +23 -0
  40. data/lib/stack-service-base/frontend_template/home/src/tests/path-prefix.test.ts +17 -0
  41. data/lib/stack-service-base/frontend_template/home/src/tests/runtime-config.test.ts +22 -0
  42. data/lib/stack-service-base/frontend_template/home/src/tests/vite-config.test.ts +16 -0
  43. data/lib/stack-service-base/frontend_template/home/src/tools/vite/build-proxy.ts +62 -0
  44. data/lib/stack-service-base/frontend_template/home/src/tools/vite/path-prefix.ts +27 -0
  45. data/lib/stack-service-base/frontend_template/home/src/tools/vite/runtime-config-dev-plugin.ts +27 -0
  46. data/lib/stack-service-base/frontend_template/home/src/tools/vite/runtime-config.ts +10 -0
  47. data/lib/stack-service-base/frontend_template/home/src/tsconfig.json +27 -0
  48. data/lib/stack-service-base/frontend_template/home/src/vite.config.ts +40 -0
  49. data/lib/stack-service-base/version.rb +1 -1
  50. metadata +47 -1
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "${service_name}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": "^24.18.0"
8
+ },
9
+ "scripts": {
10
+ "start": "vite --host 0.0.0.0",
11
+ "build": "vite build",
12
+ "build:preview": "vite build --mode preview",
13
+ "preview": "npm run build:preview && vite preview --host 0.0.0.0 --mode preview",
14
+ "typecheck": "tsc --noEmit",
15
+ "lint": "eslint .",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "check": "npm run typecheck && npm run lint && npm run test && npm run build"
19
+ },
20
+ "devDependencies": {
21
+ "@eslint/js": "9.39.4",
22
+ "@types/node": "24.13.3",
23
+ "eslint": "9.39.4",
24
+ "globals": "15.15.0",
25
+ "typescript": "5.9.3",
26
+ "typescript-eslint": "8.59.3",
27
+ "vite": "8.1.5",
28
+ "vitest": "4.1.10"
29
+ }
30
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "/api": {
3
+ "targetEnv": "BACKEND_URL",
4
+ "secure": true,
5
+ "changeOrigin": true,
6
+ "ws": true
7
+ }
8
+ }
@@ -0,0 +1 @@
1
+ window.__APP_CONFIG__ = {};
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildProxy } from '../tools/vite/build-proxy';
3
+
4
+ const rules = {
5
+ '/api': {
6
+ targetEnv: 'BACKEND_URL',
7
+ secure: true,
8
+ changeOrigin: true,
9
+ ws: true
10
+ }
11
+ };
12
+
13
+ describe('development proxy', () => {
14
+ it('builds root and prefixed routes for the configured backend', () => {
15
+ const proxy = buildProxy(
16
+ { PATH_PREFIX: '/portal', BACKEND_URL: 'https://api.example.com' },
17
+ rules
18
+ );
19
+
20
+ expect(proxy['/api']?.target).toBe('https://api.example.com');
21
+ expect(proxy['/portal/api']?.target).toBe('https://api.example.com');
22
+ expect(proxy['/portal/api']?.rewrite?.('/portal/api/status')).toBe('/api/status');
23
+ });
24
+
25
+ it('does not create routes when the backend is not configured', () => {
26
+ expect(buildProxy({}, rules)).toEqual({});
27
+ });
28
+ });
@@ -0,0 +1,23 @@
1
+ import { ESLint } from 'eslint';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { normalizePathPrefix } from '../tools/vite/path-prefix';
6
+
7
+ const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
8
+
9
+ describe('JavaScript support', () => {
10
+ it('allows JavaScript modules to import TypeScript modules', () => {
11
+ expect(normalizePathPrefix('/team/ui/')).toBe('/team/ui');
12
+ });
13
+
14
+ it('lints application JavaScript with browser globals', async () => {
15
+ const eslint = new ESLint({ cwd: projectRoot });
16
+ const [result] = await eslint.lintText(
17
+ 'document.body.dataset.path = window.location.pathname;',
18
+ { filePath: resolve(projectRoot, 'app/javascript-support.js') }
19
+ );
20
+
21
+ expect(result.messages).toEqual([]);
22
+ });
23
+ });
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { normalizePathPrefix, pathPrefixBase } from '../tools/vite/path-prefix';
3
+
4
+ describe('path prefix', () => {
5
+ it('normalizes empty and nested prefixes', () => {
6
+ expect(normalizePathPrefix(undefined)).toBe('');
7
+ expect(normalizePathPrefix(' /team/ui/ ')).toBe('/team/ui');
8
+ expect(pathPrefixBase('/team/ui')).toBe('/team/ui/');
9
+ });
10
+
11
+ it.each(['/api', '/assets', '/healthcheck', '/team//ui', '/../admin', '/admin?debug=true'])(
12
+ 'rejects an unsafe or runtime-reserved prefix: %s',
13
+ (pathPrefix) => {
14
+ expect(() => normalizePathPrefix(pathPrefix)).toThrow();
15
+ }
16
+ );
17
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildRuntimeConfigFromEnv } from '../tools/vite/runtime-config';
3
+
4
+ describe('runtime config', () => {
5
+ it('exposes only the documented browser-visible values', () => {
6
+ const runtimeConfig = buildRuntimeConfigFromEnv({
7
+ PATH_PREFIX: ' /team/ui/ ',
8
+ API_BASE_URL: ' /team/ui/api ',
9
+ LOG_LEVEL: ' debug ',
10
+ BACKEND_URL: 'https://internal-api.example.com',
11
+ AUTH_PASS: 'must-not-be-public'
12
+ });
13
+
14
+ expect(runtimeConfig).toEqual({
15
+ PATH_PREFIX: '/team/ui',
16
+ API_BASE_URL: '/team/ui/api',
17
+ LOG_LEVEL: 'debug'
18
+ });
19
+ expect(JSON.stringify(runtimeConfig)).not.toContain('internal-api');
20
+ expect(JSON.stringify(runtimeConfig)).not.toContain('must-not-be-public');
21
+ });
22
+ });
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildBase } from '../vite.config';
3
+
4
+ describe('buildBase', () => {
5
+ it('keeps the runtime placeholder in a container build', () => {
6
+ expect(buildBase('build', 'production', '/team/ui')).toBe('/__PATH_PREFIX__/');
7
+ });
8
+
9
+ it('uses the configured prefix in a local production preview', () => {
10
+ expect(buildBase('build', 'preview', '/team/ui')).toBe('/team/ui/');
11
+ });
12
+
13
+ it('uses the configured prefix in development', () => {
14
+ expect(buildBase('serve', 'development', '/team/ui')).toBe('/team/ui/');
15
+ });
16
+ });
@@ -0,0 +1,62 @@
1
+ import type { ProxyOptions } from 'vite';
2
+ import { normalizePathPrefix } from './path-prefix';
3
+
4
+ const DEFAULT_PROXY_OPTIONS: ProxyOptions = {
5
+ cookieDomainRewrite: '',
6
+ secure: true,
7
+ changeOrigin: true
8
+ };
9
+
10
+ export type ProxyRuleConfig = {
11
+ targetEnv: string;
12
+ secure?: boolean;
13
+ changeOrigin?: boolean;
14
+ ws?: boolean;
15
+ pathRewrite?: Record<string, string>;
16
+ };
17
+
18
+ export function buildProxy(
19
+ env: Record<string, string>,
20
+ rules: Record<string, ProxyRuleConfig>
21
+ ): Record<string, ProxyOptions> {
22
+ const proxy: Record<string, ProxyOptions> = {};
23
+ const pathPrefix = normalizePathPrefix(env.PATH_PREFIX);
24
+
25
+ for (const [context, rule] of Object.entries(rules)) {
26
+ const target = env[rule.targetEnv];
27
+ if (!target) continue;
28
+
29
+ proxy[context] = createProxyOptions(target, rule);
30
+ if (pathPrefix) proxy[`${pathPrefix}${context}`] = createProxyOptions(target, rule, pathPrefix);
31
+ }
32
+
33
+ return proxy;
34
+ }
35
+
36
+ function createProxyOptions(target: string, rule: ProxyRuleConfig, pathPrefix = ''): ProxyOptions {
37
+ const { pathRewrite, targetEnv: _targetEnv, ...overrides } = rule;
38
+ const shouldRewrite = Boolean(pathPrefix || pathRewrite);
39
+
40
+ return {
41
+ target,
42
+ ...DEFAULT_PROXY_OPTIONS,
43
+ ...overrides,
44
+ ...(shouldRewrite ? { rewrite: (path: string) => rewritePath(stripPathPrefix(path, pathPrefix), pathRewrite) } : {})
45
+ };
46
+ }
47
+
48
+ function stripPathPrefix(path: string, pathPrefix: string): string {
49
+ if (!pathPrefix || !path.startsWith(`${pathPrefix}/`)) return path;
50
+ return path.slice(pathPrefix.length) || '/';
51
+ }
52
+
53
+ function rewritePath(path: string, pathRewrite?: Record<string, string>): string {
54
+ if (!pathRewrite) return path;
55
+
56
+ for (const [from, to] of Object.entries(pathRewrite)) {
57
+ const rewrittenPath = path.replace(new RegExp(from), to);
58
+ if (rewrittenPath !== path) return rewrittenPath;
59
+ }
60
+
61
+ return path;
62
+ }
@@ -0,0 +1,27 @@
1
+ const PATH_PREFIX_PATTERN = /^[A-Za-z0-9_/-]+$/u;
2
+ const RESERVED_PATH_PREFIXES = new Set(['/api', '/assets', '/healthcheck']);
3
+
4
+ export function normalizePathPrefix(value: string | undefined): string {
5
+ const normalized = value?.trim().replace(/^\/+|\/+$/gu, '') ?? '';
6
+
7
+ if (!normalized) {
8
+ return '';
9
+ }
10
+
11
+ if (!PATH_PREFIX_PATTERN.test(normalized) || normalized.includes('//') || normalized.split('/').includes('..')) {
12
+ throw new Error(`Invalid PATH_PREFIX: ${value}`);
13
+ }
14
+
15
+ const pathPrefix = `/${normalized}`;
16
+
17
+ if (RESERVED_PATH_PREFIXES.has(pathPrefix)) {
18
+ throw new Error(`PATH_PREFIX is reserved by the service runtime: ${pathPrefix}`);
19
+ }
20
+
21
+ return pathPrefix;
22
+ }
23
+
24
+ export function pathPrefixBase(value: string | undefined): string {
25
+ const pathPrefix = normalizePathPrefix(value);
26
+ return pathPrefix ? `${pathPrefix}/` : '/';
27
+ }
@@ -0,0 +1,27 @@
1
+ import type { Plugin } from 'vite';
2
+ import { buildRuntimeConfigFromEnv } from './runtime-config';
3
+ import { normalizePathPrefix } from './path-prefix';
4
+
5
+ export function runtimeConfigDevPlugin(env: Record<string, string>): Plugin {
6
+ const pathPrefix = normalizePathPrefix(env.PATH_PREFIX);
7
+ const runtimeConfigPath = `${pathPrefix}/runtime-config.js`;
8
+
9
+ return {
10
+ name: 'runtime-config-dev',
11
+ configureServer(server) {
12
+ server.middlewares.use((request, response, next) => {
13
+ const requestPath = request.url?.split('?', 1)[0];
14
+
15
+ if (requestPath !== runtimeConfigPath) {
16
+ next();
17
+ return;
18
+ }
19
+
20
+ const runtimeConfig = buildRuntimeConfigFromEnv(env);
21
+ response.setHeader('Content-Type', 'application/javascript; charset=utf-8');
22
+ response.setHeader('Cache-Control', 'no-store');
23
+ response.end(`window.__APP_CONFIG__ = ${JSON.stringify(runtimeConfig)};`);
24
+ });
25
+ }
26
+ };
27
+ }
@@ -0,0 +1,10 @@
1
+ import type { RuntimeConfig } from '../../app/runtime-config.types';
2
+ import { normalizePathPrefix } from './path-prefix';
3
+
4
+ export function buildRuntimeConfigFromEnv(env: Record<string, string>): RuntimeConfig {
5
+ return {
6
+ PATH_PREFIX: normalizePathPrefix(env.PATH_PREFIX),
7
+ API_BASE_URL: env.API_BASE_URL?.trim() ?? '',
8
+ LOG_LEVEL: env.LOG_LEVEL?.trim() || 'info'
9
+ };
10
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "noUnusedLocals": true,
9
+ "noUnusedParameters": true,
10
+ "noImplicitAny": true,
11
+ "useDefineForClassFields": false,
12
+ "skipLibCheck": true,
13
+ "resolveJsonModule": true,
14
+ "allowJs": true,
15
+ "checkJs": false,
16
+ "noEmit": true
17
+ },
18
+ "include": [
19
+ "app/**/*.js",
20
+ "app/**/*.ts",
21
+ "tests/**/*.js",
22
+ "tests/**/*.ts",
23
+ "tools/**/*.js",
24
+ "tools/**/*.ts"
25
+ ],
26
+ "exclude": ["node_modules", "dist"]
27
+ }
@@ -0,0 +1,40 @@
1
+ import { defineConfig, loadEnv } from 'vite';
2
+ import proxyRules from './proxy.config.json';
3
+ import { buildProxy } from './tools/vite/build-proxy';
4
+ import { runtimeConfigDevPlugin } from './tools/vite/runtime-config-dev-plugin';
5
+ import { pathPrefixBase } from './tools/vite/path-prefix';
6
+
7
+ const DEPLOY_PATH_PLACEHOLDER = '/__PATH_PREFIX__/';
8
+
9
+ export function buildBase(
10
+ command: 'build' | 'serve',
11
+ mode: string,
12
+ pathPrefix: string | undefined
13
+ ): string {
14
+ const isContainerBuild = command === 'build' && mode !== 'preview';
15
+
16
+ return isContainerBuild ? DEPLOY_PATH_PLACEHOLDER : pathPrefixBase(pathPrefix);
17
+ }
18
+
19
+ export default defineConfig(({ command, mode }) => {
20
+ const env = loadEnv(mode, '.', '');
21
+
22
+ return {
23
+ base: buildBase(command, mode, env.PATH_PREFIX),
24
+ plugins: [runtimeConfigDevPlugin(env)],
25
+ build: {
26
+ outDir: 'dist',
27
+ emptyOutDir: true
28
+ },
29
+ server: {
30
+ port: 3000,
31
+ strictPort: true,
32
+ open: false,
33
+ proxy: buildProxy(env, proxyRules)
34
+ },
35
+ preview: {
36
+ port: 3000,
37
+ strictPort: true
38
+ }
39
+ };
40
+ });
@@ -1,3 +1,3 @@
1
1
  module StackServiceBase
2
- VERSION = '0.0.99'
2
+ VERSION = '0.0.100'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stack-service-base
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.99
4
+ version: 0.0.100
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artyom B
@@ -418,6 +418,52 @@ files:
418
418
  - lib/stack-service-base/env_fetch.rb
419
419
  - lib/stack-service-base/examples/mcp_config.ru
420
420
  - lib/stack-service-base/fiber_pool.rb
421
+ - lib/stack-service-base/frontend_template/github/.github/workflows/main.yml
422
+ - lib/stack-service-base/frontend_template/github/docker/docker-compose.yml
423
+ - lib/stack-service-base/frontend_template/gitlab-c/.gitlab-ci.yml
424
+ - lib/stack-service-base/frontend_template/gitlab-c/docker/Dockerfile.build
425
+ - lib/stack-service-base/frontend_template/gitlab-c/docker/docker-compose.yml
426
+ - lib/stack-service-base/frontend_template/gitlab-c/docker/local_build.sh
427
+ - lib/stack-service-base/frontend_template/gitlab/.gitlab-ci.yml
428
+ - lib/stack-service-base/frontend_template/gitlab/docker/Dockerfile.build
429
+ - lib/stack-service-base/frontend_template/gitlab/docker/docker-compose.yml
430
+ - lib/stack-service-base/frontend_template/gitlab/docker/local_build.sh
431
+ - lib/stack-service-base/frontend_template/home/.editorconfig
432
+ - lib/stack-service-base/frontend_template/home/.gitattributes
433
+ - lib/stack-service-base/frontend_template/home/.gitignore
434
+ - lib/stack-service-base/frontend_template/home/AGENTS.md
435
+ - lib/stack-service-base/frontend_template/home/README.md
436
+ - lib/stack-service-base/frontend_template/home/docker/.env
437
+ - lib/stack-service-base/frontend_template/home/docker/frontend/15-runtime-settings.envsh
438
+ - lib/stack-service-base/frontend_template/home/docker/frontend/40-path-prefix-settings.sh
439
+ - lib/stack-service-base/frontend_template/home/docker/frontend/45-runtime-config-settings.sh
440
+ - lib/stack-service-base/frontend_template/home/docker/frontend/Dockerfile
441
+ - lib/stack-service-base/frontend_template/home/docker/frontend/nginx.conf.template
442
+ - lib/stack-service-base/frontend_template/home/src/.dockerignore
443
+ - lib/stack-service-base/frontend_template/home/src/.env.example
444
+ - lib/stack-service-base/frontend_template/home/src/.version
445
+ - lib/stack-service-base/frontend_template/home/src/app/main.ts
446
+ - lib/stack-service-base/frontend_template/home/src/app/runtime-config.ts
447
+ - lib/stack-service-base/frontend_template/home/src/app/runtime-config.types.ts
448
+ - lib/stack-service-base/frontend_template/home/src/app/style.css
449
+ - lib/stack-service-base/frontend_template/home/src/app/vite-env.d.ts
450
+ - lib/stack-service-base/frontend_template/home/src/eslint.config.js
451
+ - lib/stack-service-base/frontend_template/home/src/index.html
452
+ - lib/stack-service-base/frontend_template/home/src/package-lock.json
453
+ - lib/stack-service-base/frontend_template/home/src/package.json
454
+ - lib/stack-service-base/frontend_template/home/src/proxy.config.json
455
+ - lib/stack-service-base/frontend_template/home/src/public/runtime-config.js
456
+ - lib/stack-service-base/frontend_template/home/src/tests/build-proxy.test.ts
457
+ - lib/stack-service-base/frontend_template/home/src/tests/javascript-support.test.js
458
+ - lib/stack-service-base/frontend_template/home/src/tests/path-prefix.test.ts
459
+ - lib/stack-service-base/frontend_template/home/src/tests/runtime-config.test.ts
460
+ - lib/stack-service-base/frontend_template/home/src/tests/vite-config.test.ts
461
+ - lib/stack-service-base/frontend_template/home/src/tools/vite/build-proxy.ts
462
+ - lib/stack-service-base/frontend_template/home/src/tools/vite/path-prefix.ts
463
+ - lib/stack-service-base/frontend_template/home/src/tools/vite/runtime-config-dev-plugin.ts
464
+ - lib/stack-service-base/frontend_template/home/src/tools/vite/runtime-config.ts
465
+ - lib/stack-service-base/frontend_template/home/src/tsconfig.json
466
+ - lib/stack-service-base/frontend_template/home/src/vite.config.ts
421
467
  - lib/stack-service-base/logging.rb
422
468
  - lib/stack-service-base/mcp/mcp_helper.rb
423
469
  - lib/stack-service-base/mcp/mcp_processor.rb