Skip to content

Commit a2541df

Browse files
Parse quoted payment config keys (#741)
1 parent f2e7ce4 commit a2541df

3 files changed

Lines changed: 133 additions & 104 deletions

File tree

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
type PaymentProviderSummary = {
2+
key: string;
3+
use: string;
4+
enabled: boolean;
5+
isDefault: boolean;
6+
};
7+
8+
export type PaymentsSummary = {
9+
path: string;
10+
defaultProvider?: string;
11+
platformFeeBps?: number;
12+
providers: PaymentProviderSummary[];
13+
};
14+
15+
export function parsePaymentsSummary(source: string, path = 'sh1pt.config.ts'): PaymentsSummary | undefined {
16+
const payments = readObjectBody(source, 'payments');
17+
if (!payments) return undefined;
18+
const providers = readObjectBody(payments, 'providers');
19+
const defaultProvider = readStringProperty(payments, 'defaultProvider');
20+
const fee = readNumberProperty(payments, 'platformFeeBps');
21+
const providerBlocks = providers ? readTopLevelObjectEntries(providers) : [];
22+
return {
23+
path,
24+
defaultProvider,
25+
platformFeeBps: fee,
26+
providers: providerBlocks.map(({ key, body }) => {
27+
const use = readStringProperty(body, 'use') ?? key;
28+
const enabled = readBooleanProperty(body, 'enabled') ?? true;
29+
return { key, use, enabled, isDefault: use === defaultProvider || key === defaultProvider };
30+
}),
31+
};
32+
}
33+
34+
function readObjectBody(source: string, property: string): string | undefined {
35+
const match = new RegExp(`(?:^|[,{\\s])${propertyKeyPattern(property)}\\s*:`).exec(source);
36+
if (!match) return undefined;
37+
const open = source.indexOf('{', match.index + match[0].length);
38+
if (open === -1) return undefined;
39+
const close = findMatchingBrace(source, open);
40+
return close === -1 ? undefined : source.slice(open + 1, close);
41+
}
42+
43+
function readTopLevelObjectEntries(source: string): Array<{ key: string; body: string }> {
44+
const entries: Array<{ key: string; body: string }> = [];
45+
const keyRe = /(?:^|,)\s*(['"]?[A-Za-z0-9_-]+['"]?)\s*:/g;
46+
let match: RegExpExecArray | null;
47+
while ((match = keyRe.exec(source))) {
48+
const rawKey = match[1];
49+
if (!rawKey) continue;
50+
const open = source.indexOf('{', keyRe.lastIndex);
51+
if (open === -1) continue;
52+
const between = source.slice(keyRe.lastIndex, open).trim();
53+
if (between.length > 0) continue;
54+
const close = findMatchingBrace(source, open);
55+
if (close === -1) continue;
56+
entries.push({ key: rawKey.replace(/^['"]|['"]$/g, ''), body: source.slice(open + 1, close) });
57+
keyRe.lastIndex = close + 1;
58+
}
59+
return entries;
60+
}
61+
62+
function findMatchingBrace(source: string, open: number): number {
63+
let depth = 0;
64+
let quote: '"' | "'" | '`' | undefined;
65+
for (let i = open; i < source.length; i += 1) {
66+
const ch = source[i];
67+
const prev = source[i - 1];
68+
if (quote) {
69+
if (ch === quote && prev !== '\\') quote = undefined;
70+
continue;
71+
}
72+
if (ch === '"' || ch === "'" || ch === '`') {
73+
quote = ch;
74+
continue;
75+
}
76+
if (ch === '{') depth += 1;
77+
if (ch === '}') {
78+
depth -= 1;
79+
if (depth === 0) return i;
80+
}
81+
}
82+
return -1;
83+
}
84+
85+
function readStringProperty(source: string, key: string): string | undefined {
86+
const match = new RegExp(`${propertyKeyPattern(key)}\\s*:\\s*['"]([^'"]+)['"]`).exec(source);
87+
return match?.[1];
88+
}
89+
90+
function readNumberProperty(source: string, key: string): number | undefined {
91+
const match = new RegExp(`${propertyKeyPattern(key)}\\s*:\\s*(\\d+)`).exec(source);
92+
return match?.[1] ? Number(match[1]) : undefined;
93+
}
94+
95+
function readBooleanProperty(source: string, key: string): boolean | undefined {
96+
const match = new RegExp(`${propertyKeyPattern(key)}\\s*:\\s*(true|false)`).exec(source);
97+
return match?.[1] === undefined ? undefined : match[1] === 'true';
98+
}
99+
100+
function propertyKeyPattern(key: string): string {
101+
return `['"]?${escapeRegExp(key)}['"]?`;
102+
}
103+
104+
function escapeRegExp(input: string): string {
105+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
106+
}

packages/cli/src/commands/config.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from 'vitest';
2-
import { parsePaymentsSummary } from './config.js';
2+
import { parsePaymentsSummary } from './config-payments.js';
33

44
describe('parsePaymentsSummary', () => {
55
it('extracts configured providers, default, and platform fee from sh1pt config text', () => {
@@ -35,4 +35,27 @@ describe('parsePaymentsSummary', () => {
3535
it('returns undefined when no payments block exists', () => {
3636
expect(parsePaymentsSummary('export default defineConfig({ name: "demo" })')).toBeUndefined();
3737
});
38+
39+
it('extracts payments when config object keys are quoted', () => {
40+
const summary = parsePaymentsSummary(`
41+
export default defineConfig({
42+
"payments": {
43+
"defaultProvider": "coinpay",
44+
"providers": {
45+
"coinpay": { "use": "payment-coinpay", "enabled": true },
46+
},
47+
"platformFeeBps": 250,
48+
},
49+
});
50+
`);
51+
52+
expect(summary).toEqual({
53+
path: 'sh1pt.config.ts',
54+
defaultProvider: 'coinpay',
55+
platformFeeBps: 250,
56+
providers: [
57+
{ key: 'coinpay', use: 'payment-coinpay', enabled: true, isDefault: true },
58+
],
59+
});
60+
});
3861
});

packages/cli/src/commands/config.ts

Lines changed: 3 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,11 @@ import { existsSync, readFileSync } from 'node:fs';
55
import { join } from 'node:path';
66
import { runSetup, type SetupContext, type SetupPromptDef, type AdapterWithSetup } from '@profullstack/sh1pt-core';
77
import { ensureInstalled, loadInstalledPackage } from '../installer.js';
8+
import { parsePaymentsSummary, type PaymentsSummary } from './config-payments.js';
89

9-
type Stack = 'node' | 'bun' | 'python' | 'rust' | 'cpp' | 'dotnet' | 'custom';
10-
11-
type PaymentProviderSummary = {
12-
key: string;
13-
use: string;
14-
enabled: boolean;
15-
isDefault: boolean;
16-
};
10+
export { parsePaymentsSummary } from './config-payments.js';
1711

18-
type PaymentsSummary = {
19-
path: string;
20-
defaultProvider?: string;
21-
platformFeeBps?: number;
22-
providers: PaymentProviderSummary[];
23-
};
12+
type Stack = 'node' | 'bun' | 'python' | 'rust' | 'cpp' | 'dotnet' | 'custom';
2413

2514
const STACKS: Array<{ value: Stack; title: string; description: string; supported: boolean }> = [
2615
{ value: 'node', title: 'Node + TypeScript + React', description: 'Next.js / Expo / Tauri / Chrome ext', supported: true },
@@ -417,25 +406,6 @@ function urlKeyFor(target: string): string {
417406
} as Record<string, string>)[target] ?? 'WEBHOOK_URL';
418407
}
419408

420-
export function parsePaymentsSummary(source: string, path = 'sh1pt.config.ts'): PaymentsSummary | undefined {
421-
const payments = readObjectBody(source, 'payments');
422-
if (!payments) return undefined;
423-
const providers = readObjectBody(payments, 'providers');
424-
const defaultProvider = readStringProperty(payments, 'defaultProvider');
425-
const fee = readNumberProperty(payments, 'platformFeeBps');
426-
const providerBlocks = providers ? readTopLevelObjectEntries(providers) : [];
427-
return {
428-
path,
429-
defaultProvider,
430-
platformFeeBps: fee,
431-
providers: providerBlocks.map(({ key, body }) => {
432-
const use = readStringProperty(body, 'use') ?? key;
433-
const enabled = readBooleanProperty(body, 'enabled') ?? true;
434-
return { key, use, enabled, isDefault: use === defaultProvider || key === defaultProvider };
435-
}),
436-
};
437-
}
438-
439409
function readPaymentsSummary(cwd: string, configPath: string): PaymentsSummary {
440410
const path = configPath.startsWith('/') ? configPath : join(cwd, configPath);
441411
if (!existsSync(path)) {
@@ -465,76 +435,6 @@ function renderPaymentsSummary(summary: PaymentsSummary): void {
465435
}
466436
}
467437

468-
function readObjectBody(source: string, property: string): string | undefined {
469-
const match = new RegExp(`(?:^|[,{\\s])${escapeRegExp(property)}\\s*:`).exec(source);
470-
if (!match) return undefined;
471-
const open = source.indexOf('{', match.index + match[0].length);
472-
if (open === -1) return undefined;
473-
const close = findMatchingBrace(source, open);
474-
return close === -1 ? undefined : source.slice(open + 1, close);
475-
}
476-
477-
function readTopLevelObjectEntries(source: string): Array<{ key: string; body: string }> {
478-
const entries: Array<{ key: string; body: string }> = [];
479-
const keyRe = /(?:^|,)\s*(['"]?[A-Za-z0-9_-]+['"]?)\s*:/g;
480-
let match: RegExpExecArray | null;
481-
while ((match = keyRe.exec(source))) {
482-
const rawKey = match[1];
483-
if (!rawKey) continue;
484-
const open = source.indexOf('{', keyRe.lastIndex);
485-
if (open === -1) continue;
486-
const between = source.slice(keyRe.lastIndex, open).trim();
487-
if (between.length > 0) continue;
488-
const close = findMatchingBrace(source, open);
489-
if (close === -1) continue;
490-
entries.push({ key: rawKey.replace(/^['"]|['"]$/g, ''), body: source.slice(open + 1, close) });
491-
keyRe.lastIndex = close + 1;
492-
}
493-
return entries;
494-
}
495-
496-
function findMatchingBrace(source: string, open: number): number {
497-
let depth = 0;
498-
let quote: '"' | "'" | '`' | undefined;
499-
for (let i = open; i < source.length; i += 1) {
500-
const ch = source[i];
501-
const prev = source[i - 1];
502-
if (quote) {
503-
if (ch === quote && prev !== '\\') quote = undefined;
504-
continue;
505-
}
506-
if (ch === '"' || ch === "'" || ch === '`') {
507-
quote = ch;
508-
continue;
509-
}
510-
if (ch === '{') depth += 1;
511-
if (ch === '}') {
512-
depth -= 1;
513-
if (depth === 0) return i;
514-
}
515-
}
516-
return -1;
517-
}
518-
519-
function readStringProperty(source: string, key: string): string | undefined {
520-
const match = new RegExp(`${escapeRegExp(key)}\\s*:\\s*['"]([^'"]+)['"]`).exec(source);
521-
return match?.[1];
522-
}
523-
524-
function readNumberProperty(source: string, key: string): number | undefined {
525-
const match = new RegExp(`${escapeRegExp(key)}\\s*:\\s*(\\d+)`).exec(source);
526-
return match?.[1] ? Number(match[1]) : undefined;
527-
}
528-
529-
function readBooleanProperty(source: string, key: string): boolean | undefined {
530-
const match = new RegExp(`${escapeRegExp(key)}\\s*:\\s*(true|false)`).exec(source);
531-
return match?.[1] === undefined ? undefined : match[1] === 'true';
532-
}
533-
534-
function escapeRegExp(input: string): string {
535-
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
536-
}
537-
538438
// Build the SetupContext the CLI hands to every adapter.setup(). Today
539439
// secrets live in-process + logged; a real vault lands once `sh1pt login`
540440
// has an API to write against.

0 commit comments

Comments
 (0)