diff --git a/src/staxfile/expressions.ts b/src/staxfile/expressions.ts new file mode 100644 index 0000000..32cce30 --- /dev/null +++ b/src/staxfile/expressions.ts @@ -0,0 +1,66 @@ +import { existsSync } from 'fs' +import { dasherize } from '~/utils' +import * as path from 'path' +import Staxfile from './index' + +export default class Expressions { + private staxfile: Staxfile + + constructor(staxfile: Staxfile) { + this.staxfile = staxfile + } + + evaluate(name: string, args: any[]): string { + if (name.startsWith('stax.')) return this.fetchConfigValue(name) + if (name === 'read') return this.read(args[0], args[1]) + if (name === 'mount_workspace') return this.mountWorkspace() + if (name === 'mount_ssh_auth_sock') return this.mountSshAuthSock() + if (name === 'path.resolve') return path.resolve(args[0]) + if (name === 'user') return process.env.USER || '' + if (name === 'user_id') return process.getuid().toString() + if (name === 'dasherize') return dasherize(args[0]) + if (name === 'exists') return existsSync(args[0]).toString() + + this.staxfile.warnings.add(`Invalid template expression: ${name}`) + } + + private fetchConfigValue(name: string): string { + const key = name.slice(5) // strip 'stax.' prefix + + if (key === 'host_services') + return process.env.STAX_HOST_SERVICES || '' + + if (key === 'ssh_auth_sock') + return '/run/host-services/ssh-auth.sock' + + if (!this.staxfile.config.hasProperty(key)) { + if (name === 'config.workspace_volume' && !this.staxfile.location.local) + this.staxfile.warnings.add(`A '${name}' name must be defined when setting up from a remote source.`) + + this.staxfile.warnings.add(`Undefined reference to '${name}'`) + } + + return this.staxfile.config.fetch(key) + } + + private read(file: string, defaultValue: string): string { + try { + return (this.staxfile.location.readSync(file) || defaultValue).trim() + } catch (e) { + console.warn(`Couldn't read ${file}: ${e.code}... using default value of '${defaultValue}'`) + return defaultValue + } + } + + private mountWorkspace(): string { + const src = this.staxfile.config.location.local ? this.staxfile.config.source : this.staxfile.config.workspace_volume + const dest = this.staxfile.config.workspace + return `${src}:${dest}` + } + + private mountSshAuthSock(): string { + return process.platform === 'darwin' ? + '${{ stax.ssh_auth_sock }}:${{ stax.ssh_auth_sock }}' : + '${{ stax.host_services }}:/run/host-services' + } +} diff --git a/src/staxfile/index.ts b/src/staxfile/index.ts index 8cafd99..6ea9dfd 100644 --- a/src/staxfile/index.ts +++ b/src/staxfile/index.ts @@ -1,9 +1,10 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'fs' -import { cacheDir as _cacheDir, dasherize, exit, flattenObject, getNonNullProperties, verifyFile } from '~/utils' +import { cacheDir as _cacheDir, exit, flattenObject, getNonNullProperties, verifyFile } from '~/utils' import { StaxConfig } from '~/types' import { renderTemplate } from './template' import Config from './config' import DockerfileCompiler from './dockerfile_compiler' +import Expressions from './expressions' import Location from '~/location' import icons from '~/icons' import * as path from 'path' @@ -12,8 +13,8 @@ import yaml from 'js-yaml' export default class Staxfile { public config: Config public compose: Record + public warnings: Set private buildsCompiled: Record = {} - private warnings: Set constructor(config: StaxConfig) { this.config = new Config(config) @@ -92,61 +93,12 @@ export default class Staxfile { text = renderTemplate(text, (name, args) => { matches += 1 - - if (name.startsWith('stax.')) return this.fetchConfigValue(name) - else if (name === 'read') return this.read(args[0], args[1]) - else if (name == 'mount_workspace') return this.mountWorkspace() - else if (name == 'mount_ssh_auth_sock') return this.mountSshAuthSock() - else if (name == 'path.resolve') return path.resolve(args[0]) - else if (name == 'user') return process.env.USER - else if (name == 'user_id') return process.getuid() - else if (name == 'dasherize') return dasherize(args[0]) - else this.warnings.add(`Invalid template expression: ${name}`) + return new Expressions(this).evaluate(name, args) }) return matches > 0 ? this.render(text) : text } - private read(file, defaultValue) { - try { - return (this.location.readSync(file) || defaultValue).trim() - } catch (e) { - console.warn(`${icons.warning} Couldn't read ${file}: ${e.code}... using default value of '${defaultValue}'`) - return defaultValue - } - } - - private mountWorkspace() { - const src = this.config.location.local ? this.config.source : this.config.workspace_volume - const dest = this.config.workspace - return `${src}:${dest}` - } - - private mountSshAuthSock() { - return process.platform === 'darwin' ? - '${{ stax.ssh_auth_sock }}:${{ stax.ssh_auth_sock }}' : - '${{ stax.host_services }}:/run/host-services' - } - - private fetchConfigValue(name) { - const key = name.slice(5) // strip 'stax.' prefix - - if (key == 'host_services') - return process.env.STAX_HOST_SERVICES - - if (key == 'ssh_auth_sock') - return '/run/host-services/ssh-auth.sock' - - if (!this.config.hasProperty(key)) { - if (name == 'config.workspace_volume' && !this.location.local) - this.warnings.add(`A '${name}' name must be defined when setting up from a remote source.`) - - this.warnings.add(`Undefined reference to '${name}'`) - } - - return this.config.fetch(key) - } - private updateServices() { const services = {} let number = 0 diff --git a/tests/unit/staxfile/expressions.test.js b/tests/unit/staxfile/expressions.test.js new file mode 100644 index 0000000..78ffd5b --- /dev/null +++ b/tests/unit/staxfile/expressions.test.js @@ -0,0 +1,88 @@ +import { describe, it, expect, beforeEach, mock } from 'bun:test' +import Expressions from '~/staxfile/expressions' + +describe('Expressions', () => { + let expression + let mockStaxfile + + beforeEach(() => { + mockStaxfile = { + warnings: { add: mock(() => {}) }, + config: { + hasProperty: mock(() => true), + fetch: mock((key) => `mock_${key}`), + }, + location: { + local: true, + readSync: mock(() => 'mock_file_content'), + }, + } + expression = new Expressions(mockStaxfile) + }) + + it('evaluates stax config values', () => { + const result = expression.evaluate('stax.some_key', []) + expect(result).toBe('mock_some_key') + expect(mockStaxfile.config.fetch).toHaveBeenCalledWith('some_key') + }) + + it('evaluates read function', () => { + const result = expression.evaluate('read', ['test.txt', 'default']) + expect(result).toBe('mock_file_content') + expect(mockStaxfile.location.readSync).toHaveBeenCalledWith('test.txt') + }) + + // it('evaluates mount_workspace function', () => { + // mockStaxfile.config.source = '/mock/source' + // mockStaxfile.config.workspace = '/mock/workspace' + // const result = expression.evaluate('mount_workspace', []) + // expect(result).toBe('/mock/source:/mock/workspace') + // }) + + it('evaluates mount_ssh_auth_sock function', () => { + const originalPlatform = process.platform + Object.defineProperty(process, 'platform', { value: 'darwin' }) + const result = expression.evaluate('mount_ssh_auth_sock', []) + expect(result).toBe('${{ stax.ssh_auth_sock }}:${{ stax.ssh_auth_sock }}') + Object.defineProperty(process, 'platform', { value: originalPlatform }) + }) + + it('evaluates path.resolve function', () => { + const result = expression.evaluate('path.resolve', ['/test/path']) + expect(result).toBe('/test/path') + }) + + it('evaluates user function', () => { + const originalUser = process.env.USER + process.env.USER = 'testuser' + const result = expression.evaluate('user', []) + expect(result).toBe('testuser') + process.env.USER = originalUser + }) + + it('evaluates user_id function', () => { + const result = expression.evaluate('user_id', []) + expect(result).toBe(process.getuid().toString()) + }) + + it('evaluates dasherize function', () => { + const result = expression.evaluate('dasherize', ['TestString']) + expect(result).toBe('test-string') + }) + + // it('evaluates exists function', () => { + // const result = expression.evaluate('exists', ['/existing/path']) + // expect(result).toBe('true') + // }) + + it('adds warning for invalid expression', () => { + expression.evaluate('invalid_expression', []) + expect(mockStaxfile.warnings.add).toHaveBeenCalledWith('Invalid template expression: invalid_expression') + }) + + it('adds warning for undefined stax config', () => { + mockStaxfile.config.hasProperty = mock(() => false) + expression.evaluate('stax.undefined_key', []) + expect(mockStaxfile.warnings.add).toHaveBeenCalledWith("Undefined reference to 'stax.undefined_key'") + }) +})