From efeeb23e94da02a26a495063994cbba27c551ede Mon Sep 17 00:00:00 2001 From: Kien La Date: Thu, 26 Sep 2024 18:47:06 -0400 Subject: [PATCH] stuff --- src/app.ts | 10 +--- src/container.ts | 2 +- src/location.ts | 4 +- src/staxfile/config.ts | 1 + src/staxfile/expressions.ts | 10 +++- src/staxfile/index.ts | 21 +++---- tests/unit/staxfile/expressions.test.js | 75 +++++++++++++------------ tests/unit/staxfile/template.test.js | 52 ++++++++--------- 8 files changed, 91 insertions(+), 84 deletions(-) diff --git a/src/app.ts b/src/app.ts index 027b430..f6e3f29 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,5 @@ import { readFileSync, rmSync } from 'fs' -import { cacheDir, exit, fileExists, pp } from '~/utils' +import { cacheDir, exit, fileExists, pp, isEmpty } from '~/utils' import { StaxConfig, SetupOptions, FindContainerOptions } from '~/types' import { linkSshAuthSock } from './host_services' import Staxfile from '~/staxfile' @@ -83,10 +83,10 @@ export default class App { const app = App.find(staxfile.context, staxfile.app) if (options.duplicate || (!options.rebuild && !staxfile.config.location.local)) - await app.primary.exec(`git clone ${staxfile.config.source} ${staxfile.compose.stax.workspace}`) + await app.primary.exec(`sh -c '[ -z "$(ls -A ${staxfile.compose.stax.workspace})" ] && git clone ${staxfile.config.source} ${staxfile.compose.stax.workspace} || echo "Directory not empty. Skipping git clone."'`) if (options.duplicate || !options.rebuild) - await Promise.all(app.containers.map(container => container.runHook('after_setup'))) + await app.primary.runHook('after_setup') return app } @@ -162,10 +162,6 @@ export default class App { return Promise.all(this.containers.map(container => container.restart())) } - async runHooks() { - this.containers.forEach(async container => container.runHooks()) - } - addAlias(alias: string) { const aliases = settings.read('aliases') || {} diff --git a/src/container.ts b/src/container.ts index 79d8fe0..ff0cec4 100644 --- a/src/container.ts +++ b/src/container.ts @@ -174,7 +174,7 @@ export default class Container { } async runHook(type) { - let hook = this.labels[`stax.hooks.${type}`] + let hook = this.labels[`stax.${type}`] if (!hook) return if (existsSync(hook)) diff --git a/src/location.ts b/src/location.ts index 42e57eb..5787645 100644 --- a/src/location.ts +++ b/src/location.ts @@ -20,7 +20,7 @@ export default class Location { } static isGitUrl(url: string): boolean { - return url && (url.startsWith('git@') || url.startsWith('https://')) + return url && (url.endsWith('.git') || url.startsWith('https://')) } static from(context: string, app: string, location: string): Location { @@ -111,6 +111,6 @@ class ContainerLocation extends GitLocation { } file = path.join(this.container.config.workspace, file) - return capture(`docker container exec ${this.container.containerName} cat "${file}"`) + return capture(`docker container exec ${this.container.containerName} sh -c 'cat "${file}" || true'`) } } diff --git a/src/staxfile/config.ts b/src/staxfile/config.ts index 69d760c..1d43cf4 100644 --- a/src/staxfile/config.ts +++ b/src/staxfile/config.ts @@ -14,6 +14,7 @@ export default class Config implements StaxConfig { public workspace!: string public workspace_volume!: string public vars!: Record + public after_setup!: string constructor(config: StaxConfig | Record | undefined = undefined) { if (config) diff --git a/src/staxfile/expressions.ts b/src/staxfile/expressions.ts index b37bda9..b7ef13f 100644 --- a/src/staxfile/expressions.ts +++ b/src/staxfile/expressions.ts @@ -11,6 +11,10 @@ export default class Expressions { this.staxfile = staxfile } + clearCache() { + Expressions.cache = {} + } + async evaluate(name: string, args: any[]): Promise { const cacheKey = this.getCacheKey(name, args) @@ -87,8 +91,12 @@ export default class Expressions { return `${src}:${dest}` } + private platform(): string { + return process.platform + } + private mountSshAuthSock(): string { - return process.platform === 'darwin' ? + return this.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 fa4d715..7b3b8d9 100644 --- a/src/staxfile/index.ts +++ b/src/staxfile/index.ts @@ -78,13 +78,15 @@ export default class Staxfile { this.warnings = new Set() this.compose = yaml.load(readFileSync(this.staxfile, 'utf-8')) + // render the stax section first since we need to update this.config with the values there this.compose.stax = await this.renderCompose(this.compose.stax) this.config = new Config({ ...this.config, ...this.compose.stax }) this.compose = await this.renderCompose(this.compose) this.updateServices() - this.compose = await this.renderCompose(this.compose) // Re-render after updating services - // console.log(this.compose); console.log(this.config);process.exit() + + // need to re-render after updating services since template expressions may have been added + this.compose = await this.renderCompose(this.compose) if (this.generatedWarnings.length > 0) exit(1, this.generatedWarnings.join('\n')) @@ -104,12 +106,7 @@ export default class Staxfile { text = await renderTemplate(text, async (name, args) => { matches += 1 - - const x = await this.expressions.evaluate(name, args) - if (name == 'prompt') { - console.log(name, args, x) - } - return x + return await this.expressions.evaluate(name, args) }) return matches > 0 ? await this.render(text) : text @@ -152,11 +149,11 @@ export default class Staxfile { const hooks = [ 'after_setup' ] for (const hook of hooks) { - if (labels[`stax.hooks.${hook}`]) { - if (existsSync(labels[`stax.hooks.${hook}`])) { - const file = path.resolve(labels[`stax.hooks.${hook}`]) + if (labels[`stax.${hook}`]) { + if (existsSync(labels[`stax.${hook}`])) { + const file = path.resolve(labels[`stax.${hook}`]) verifyFile(file, `Hook file not found for '${hook}'`) - labels[`stax.hooks.${hook}`] = file + labels[`stax.${hook}`] = file } } } diff --git a/tests/unit/staxfile/expressions.test.js b/tests/unit/staxfile/expressions.test.js index 12763d2..94fe53f 100644 --- a/tests/unit/staxfile/expressions.test.js +++ b/tests/unit/staxfile/expressions.test.js @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, mock } from 'bun:test' +import { describe, it, expect, afterEach, beforeEach, mock } from 'bun:test' import Expressions from '~/staxfile/expressions' import Staxfile from '~/staxfile' import os from 'os' @@ -13,9 +13,13 @@ describe('Expressions', () => { expression = new Expressions(staxfile) }) - it('evaluates undefined stax config values', () => { - const result = expression.evaluate('stax.app', []) - expect(result).toBe('tests') + afterEach(() => { + mock.restore() + expression.clearCache() + }) + + it('evaluates undefined stax config values', async () => { + expect(await expression.evaluate('stax.app', [])).toBe('tests') }) // it('evaluates read function', () => { @@ -23,55 +27,56 @@ describe('Expressions', () => { // expect(result).toContain('import { describe, it, expect, beforeEach } from \'bun:test\'') // }) - it('evaluates mount_workspace function', () => { - const result = expression.evaluate('mount_workspace', []) + it('evaluates mount_workspace function', async () => { + const result = await expression.evaluate('mount_workspace', []) expect(result).toBe(`${path.resolve('./tests')}:/workspaces/tests`) }) - it('evaluates mount_ssh_auth_sock function', () => { - const result = expression.evaluate('mount_ssh_auth_sock', []) - expect(result).toBe('${{ stax.host_services }}:/run/host-services') + it('evaluates mount_ssh_auth_sock function for Darwin', async () => { + expression.platform = mock(() => 'darwin') + expect(await expression.evaluate('mount_ssh_auth_sock', [])).toBe('${{ stax.ssh_auth_sock }}:${{ stax.ssh_auth_sock }}') + }) + + it('evaluates mount_ssh_auth_sock function for Linux', async () => { + expression.platform = mock(() => 'linux') + expect(await expression.evaluate('mount_ssh_auth_sock', [])).toBe('${{ stax.host_services }}:/run/host-services') }) - it('evaluates resolve function', () => { - const result = expression.evaluate('resolve', ['/test/path']) - expect(result).toBe('/test/path') + it('evaluates resolve function', async () => { + expect(await expression.evaluate('resolve', ['/test/path'])).toBe('/test/path') }) - it('evaluates user function', () => { - const result = expression.evaluate('user', []) - expect(result).toBe(os.userInfo().username) + it('evaluates user function', async () => { + expect(await expression.evaluate('user', [])).toBe(os.userInfo().username) }) - it('evaluates user_id function', () => { - const result = expression.evaluate('user_id', []) - expect(result).toBe(process.getuid().toString()) + it('evaluates user_id function', async () => { + expect(await expression.evaluate('user_id', [])).toBe(process.getuid().toString()) }) - it('evaluates dasherize function', () => { - const result = expression.evaluate('dasherize', ['TestString']) - expect(result).toBe('test-string') + it('evaluates dasherize function', async () => { + expect(await expression.evaluate('dasherize', ['TestString'])).toBe('test-string') }) - it('evaluates dasherize function with stax.app', () => { + it('evaluates dasherize function with stax.app', async () => { staxfile.config.set('app', 'MyTestApp') - const result = expression.evaluate('dasherize', ['stax.app']) + const result = await expression.evaluate('dasherize', ['stax.app']) expect(result).toBe('my-test-app') }) - it('evaluates dasherize function with undefined stax config', () => { - const result = expression.evaluate('dasherize', ['stax.undefined_key']) + it('evaluates dasherize function with undefined stax config', async () => { + const result = await expression.evaluate('dasherize', ['stax.undefined_key']) expect(result).toBeUndefined() expect(staxfile.warnings).toContain("Undefined reference to 'stax.undefined_key'") }) - it('adds warning for invalid expression', () => { - expression.evaluate('invalid_expression', []) + it('adds warning for invalid expression', async () => { + await expression.evaluate('invalid_expression', []) expect(staxfile.warnings).toContain('Invalid template expression: invalid_expression') }) - it('adds warning for undefined stax config', () => { - expression.evaluate('stax.undefined_key', []) + it('adds warning for undefined stax config', async () => { + await expression.evaluate('stax.undefined_key', []) console.log(staxfile.warnings) expect(staxfile.warnings).toContain("Undefined reference to 'stax.undefined_key'") }) @@ -94,21 +99,21 @@ describe('Expressions', () => { expressions = new Expressions(mockStaxfile) }) - it('returns true when pattern is found in file', () => { + it('returns true when pattern is found in file', async () => { mockStaxfile.location.readSync.mockImplementation(() => 'Hello, world!') - const result = expressions.evaluate('test', ['test.txt', 'world']) + const result = await expressions.evaluate('test', ['test.txt', 'world']) expect(result).toBe('true') }) - it('returns false when pattern is not found in file', () => { + it('returns false when pattern is not found in file', async () => { mockStaxfile.location.readSync.mockImplementation(() => 'Hello, world!') - const result = expressions.evaluate('test', ['test.txt', 'foo']) + const result = await expressions.evaluate('test', ['test.txt', 'foo']) expect(result).toBe('false') }) - it('uses default value when file cannot be read', () => { + it('uses default value when file cannot be read', async () => { mockStaxfile.location.readSync.mockImplementation(() => { throw new Error('File not found') }) - const result = expressions.evaluate('test', ['nonexistent.txt', 'pattern']) + const result = await expressions.evaluate('test', ['nonexistent.txt', 'pattern']) expect(result).toBe('false') }) }) diff --git a/tests/unit/staxfile/template.test.js b/tests/unit/staxfile/template.test.js index 238c5e1..bf8ea3a 100644 --- a/tests/unit/staxfile/template.test.js +++ b/tests/unit/staxfile/template.test.js @@ -2,81 +2,81 @@ import { expect, it, describe } from 'bun:test' import { renderTemplate } from '~/staxfile/template' describe('renderTemplate', () => { - it('replaces tokens with callback results', () => { + it('replaces tokens with callback results', async () => { const template = 'Hello, ${{ name John Doe }}! Your age is ${{ age 30 }}.' - const callback = (name, args) => { + const callback = async (name, args) => { if (name === 'name') return args.join(' ') if (name === 'age') return `${parseInt(args[0]) + 1}` return '' } - const result = renderTemplate(template, callback) + const result = await renderTemplate(template, callback) expect(result).toBe('Hello, John Doe! Your age is 31.') }) - it('handles tokens without arguments', () => { + it('handles tokens without arguments', async () => { const template = 'The current year is ${{ year }}.' - const callback = (name) => name === 'year' ? '2024' : '' + const callback = async (name) => name === 'year' ? '2024' : '' - const result = renderTemplate(template, callback) + const result = await renderTemplate(template, callback) expect(result).toBe('The current year is 2024.') }) - it('ignores malformed tokens', () => { + it('ignores malformed tokens', async () => { const template = 'This is a ${{ malformed token }} and this is correct ${{ correct token }}.' - const callback = (name, args) => name === 'correct' ? 'CORRECT' : 'IGNORED' + const callback = async (name, args) => name === 'correct' ? 'CORRECT' : 'IGNORED' - const result = renderTemplate(template, callback) + const result = await renderTemplate(template, callback) expect(result).toBe('This is a IGNORED and this is correct CORRECT.') }) - it('handles multiple tokens in a single line', () => { + it('handles multiple tokens in a single line', async () => { const template = '${{ greeting Hello }} ${{ name John }}! Today is ${{ day Monday }}.' - const callback = (name, args) => args.join(' ') + const callback = async (name, args) => args.join(' ') - const result = renderTemplate(template, callback) + const result = await renderTemplate(template, callback) expect(result).toBe('Hello John! Today is Monday.') }) - it('handles simple arguments', () => { + it('handles simple arguments', async () => { const input = "${{ test arg1 arg2 arg3 }}" - const result = renderTemplate(input, (name, args) => `${name}:${args.join(',')}`) + const result = await renderTemplate(input, async (name, args) => `${name}:${args.join(',')}`) expect(result).toBe("test:arg1,arg2,arg3") }) - it('handles single-quoted arguments', () => { + it('handles single-quoted arguments', async () => { const input = "${{ test 'arg with spaces' normal_arg }}" - const result = renderTemplate(input, (name, args) => `${name}:${args.join(',')}`) + const result = await renderTemplate(input, async (name, args) => `${name}:${args.join(',')}`) expect(result).toBe("test:arg with spaces,normal_arg") }) - it('handles multi-part quoted arguments', () => { + it('handles multi-part quoted arguments', async () => { const input = "${{ test 'arg with multiple parts' normal_arg }}" - const result = renderTemplate(input, (name, args) => `${name}:${args.join(',')}`) + const result = await renderTemplate(input, async (name, args) => `${name}:${args.join(',')}`) expect(result).toBe("test:arg with multiple parts,normal_arg") }) - it('handles mixed quoted and unquoted arguments', () => { + it('handles mixed quoted and unquoted arguments', async () => { const input = "${{ test normal_arg 'quoted arg' another_normal 'multi part quoted arg' }}" - const result = renderTemplate(input, (name, args) => `${name}:${args.join(',')}`) + const result = await renderTemplate(input, async (name, args) => `${name}:${args.join(',')}`) expect(result).toBe("test:normal_arg,quoted arg,another_normal,multi part quoted arg") }) - it('handles empty arguments', () => { + it('handles empty arguments', async () => { const input = "${{ test }}" - const result = renderTemplate(input, (name, args) => `${name}:${args.length}`) + const result = await renderTemplate(input, async (name, args) => `${name}:${args.length}`) expect(result).toBe("test:0") }) - it('handles unclosed quotes', () => { + it('handles unclosed quotes', async () => { const input = "${{ test 'unclosed quote argument }}" - const result = renderTemplate(input, (name, args) => `${name}:${args.join(',')}`) + const result = await renderTemplate(input, async (name, args) => `${name}:${args.join(',')}`) expect(result).toBe("test:unclosed quote argument") }) - it('handles multiple template expressions', () => { + it('handles multiple template expressions', async () => { const input = "Hello ${{ test arg1 }} world ${{ test2 'arg2 with spaces' }}" - const result = renderTemplate(input, (name, args) => `${name}:${args.join('|')}`) + const result = await renderTemplate(input, async (name, args) => `${name}:${args.join('|')}`) expect(result).toBe("Hello test:arg1 world test2:arg2 with spaces") }) })