@membranehq/cli 0.1.1

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 (80) hide show
  1. package/.turbo/turbo-build.log +9 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +54 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +335 -0
  6. package/package.json +46 -0
  7. package/scripts/add-shebang.sh +6 -0
  8. package/scripts/prepare-package-json.ts +29 -0
  9. package/src/agent.tsx +50 -0
  10. package/src/cli.ts +72 -0
  11. package/src/commands/open.command.ts +51 -0
  12. package/src/commands/pull.command.ts +75 -0
  13. package/src/commands/push.command.ts +79 -0
  14. package/src/commands/test.command.ts +99 -0
  15. package/src/components/AddMcpServerScreen.tsx +215 -0
  16. package/src/components/AgentStatus.tsx +15 -0
  17. package/src/components/Main.tsx +64 -0
  18. package/src/components/OverviewSection.tsx +24 -0
  19. package/src/components/PersonalAccessTokenInput.tsx +56 -0
  20. package/src/components/RecentChanges.tsx +65 -0
  21. package/src/components/SelectWorkspace.tsx +112 -0
  22. package/src/components/Setup.tsx +121 -0
  23. package/src/components/WorkspaceStatus.tsx +61 -0
  24. package/src/contexts/FileWatcherContext.tsx +81 -0
  25. package/src/index.ts +27 -0
  26. package/src/legacy/commands/pullWorkspace.ts +70 -0
  27. package/src/legacy/commands/pushWorkspace.ts +246 -0
  28. package/src/legacy/integrationElements.ts +78 -0
  29. package/src/legacy/push/types.ts +17 -0
  30. package/src/legacy/reader/index.ts +113 -0
  31. package/src/legacy/types.ts +17 -0
  32. package/src/legacy/util.ts +149 -0
  33. package/src/legacy/workspace-elements/connectors.ts +397 -0
  34. package/src/legacy/workspace-elements/index.ts +27 -0
  35. package/src/legacy/workspace-tools/commands/pullWorkspace.ts +70 -0
  36. package/src/legacy/workspace-tools/integrationElements.ts +78 -0
  37. package/src/legacy/workspace-tools/util.ts +149 -0
  38. package/src/mcp/server-status.ts +27 -0
  39. package/src/mcp/server.ts +36 -0
  40. package/src/mcp/tools/getTestAccessToken.ts +32 -0
  41. package/src/modules/api/account-api-client.ts +89 -0
  42. package/src/modules/api/index.ts +3 -0
  43. package/src/modules/api/membrane-api-client.ts +116 -0
  44. package/src/modules/api/workspace-api-client.ts +11 -0
  45. package/src/modules/config/cwd-context.tsx +11 -0
  46. package/src/modules/config/project/getAgentVersion.ts +16 -0
  47. package/src/modules/config/project/index.ts +8 -0
  48. package/src/modules/config/project/paths.ts +25 -0
  49. package/src/modules/config/project/readProjectConfig.ts +27 -0
  50. package/src/modules/config/project/useProjectConfig.tsx +103 -0
  51. package/src/modules/config/system/index.ts +35 -0
  52. package/src/modules/file-watcher/index.ts +166 -0
  53. package/src/modules/file-watcher/types.ts +14 -0
  54. package/src/modules/setup/steps.ts +9 -0
  55. package/src/modules/setup/useSetup.ts +16 -0
  56. package/src/modules/status/useStatus.ts +16 -0
  57. package/src/modules/workspace-element-service/constants.ts +121 -0
  58. package/src/modules/workspace-element-service/getTypeAndKeyFromPath.ts +69 -0
  59. package/src/modules/workspace-element-service/index.ts +304 -0
  60. package/src/testing/environment.ts +172 -0
  61. package/src/testing/runners/base.runner.ts +27 -0
  62. package/src/testing/runners/test.runner.ts +123 -0
  63. package/src/testing/scripts/generate-test-report.ts +757 -0
  64. package/src/testing/test-suites/base.ts +92 -0
  65. package/src/testing/test-suites/data-collection.ts +128 -0
  66. package/src/testing/testers/base.ts +115 -0
  67. package/src/testing/testers/create.ts +273 -0
  68. package/src/testing/testers/delete.ts +155 -0
  69. package/src/testing/testers/find-by-id.ts +135 -0
  70. package/src/testing/testers/list.ts +110 -0
  71. package/src/testing/testers/match.ts +149 -0
  72. package/src/testing/testers/search.ts +148 -0
  73. package/src/testing/testers/spec.ts +30 -0
  74. package/src/testing/testers/update.ts +284 -0
  75. package/src/utils/auth.ts +19 -0
  76. package/src/utils/constants.ts +27 -0
  77. package/src/utils/fields.ts +83 -0
  78. package/src/utils/logger.ts +106 -0
  79. package/src/utils/templating.ts +50 -0
  80. package/tsconfig.json +21 -0
@@ -0,0 +1,30 @@
1
+ import { DataCollectionSpec } from '@integration-app/sdk'
2
+
3
+ import { TestEnvironment } from '../environment'
4
+ import { BaseTester } from './base'
5
+
6
+ export class DataCollectionSpecTester extends BaseTester<undefined> {
7
+ private dataCollection: DataCollectionSpec
8
+
9
+ constructor({
10
+ environment,
11
+ dataCollectionKey,
12
+ dataCollection,
13
+ }: {
14
+ environment: TestEnvironment
15
+ dataCollectionKey: string
16
+ dataCollection: DataCollectionSpec
17
+ }) {
18
+ super({
19
+ environment,
20
+ path: `data/${dataCollectionKey}/spec`,
21
+ })
22
+
23
+ this.dataCollection = dataCollection
24
+ }
25
+
26
+ async run(): Promise<void> {
27
+ const fieldCount = Object.keys(this.dataCollection.fieldsSchema).length
28
+ await this.assert(() => fieldCount > 0, 'Fields schema has field definitions')
29
+ }
30
+ }
@@ -0,0 +1,284 @@
1
+ import {
2
+ DataCollectionSpec,
3
+ excludeWriteOnlyFieldsFromSchema,
4
+ getDataCollectionUpdateFields,
5
+ valueToSchema,
6
+ extractIntegrationAppErrorData,
7
+ walkSchema,
8
+ makeDataLocationPath,
9
+ } from '@integration-app/sdk'
10
+ import chalk from 'chalk'
11
+
12
+ import { TestEnvironment } from '../environment'
13
+ import { BaseTester } from './base'
14
+ import { getNotMatchingSubFields } from '../../utils/fields'
15
+
16
+ interface DataCollectionUpdateTestConfig {
17
+ input: {
18
+ id: string
19
+ fields: Record<string, any>
20
+ }
21
+ }
22
+
23
+ interface ReferenceCollection {
24
+ key: string
25
+ parameters?: Record<string, any>
26
+ }
27
+
28
+ interface ExampleRecord {
29
+ id: string
30
+ fields: Record<string, any>
31
+ }
32
+
33
+ export class DataCollectionUpdateTester extends BaseTester<DataCollectionUpdateTestConfig> {
34
+ private dataCollectionKey: string
35
+ private dataCollection: DataCollectionSpec
36
+
37
+ constructor({
38
+ environment,
39
+ dataCollectionKey,
40
+ dataCollection,
41
+ }: {
42
+ environment: TestEnvironment
43
+ dataCollectionKey: string
44
+ dataCollection: DataCollectionSpec
45
+ }) {
46
+ super({
47
+ environment,
48
+ path: `data/${dataCollectionKey}/update`,
49
+ })
50
+
51
+ this.dataCollectionKey = dataCollectionKey
52
+ this.dataCollection = dataCollection
53
+ }
54
+
55
+ async run(config: DataCollectionUpdateTestConfig): Promise<void> {
56
+ const id =
57
+ (
58
+ this.environment.state[this.dataCollectionKey] as {
59
+ createdRecordId: string
60
+ }
61
+ )?.createdRecordId || config?.input?.id
62
+ if (!id) {
63
+ throw new Error(`No ID found in state or config for ${this.dataCollectionKey} update`)
64
+ }
65
+
66
+ const updateResponse = await this.environment.client
67
+ .connection(this.environment.connectionId)
68
+ .dataCollection(this.dataCollectionKey)
69
+ .update({ ...config.input, id })
70
+
71
+ await this.assert(() => !!updateResponse.id, 'Returned ID of updated record')
72
+
73
+ if (!this.environment.state[this.dataCollectionKey]) {
74
+ this.environment.state[this.dataCollectionKey] = {}
75
+ }
76
+ ;(
77
+ this.environment.state[this.dataCollectionKey] as {
78
+ createdRecordId?: string
79
+ }
80
+ ).createdRecordId = updateResponse.id
81
+
82
+ if (this.dataCollection.findById) {
83
+ const findByIdResponse = await this.environment.client
84
+ .connection(this.environment.connectionId)
85
+ .dataCollection(this.dataCollectionKey)
86
+ .findById({
87
+ id: updateResponse.id,
88
+ })
89
+
90
+ await this.assert(() => !!findByIdResponse.record, 'Record is returned from findById')
91
+
92
+ const updateFieldsSchema = getDataCollectionUpdateFields(this.dataCollection)
93
+ const schemaToCompare = excludeWriteOnlyFieldsFromSchema(updateFieldsSchema)
94
+ const sentFieldsToCheck = valueToSchema(config.input.fields, schemaToCompare, {
95
+ skipUnknownFields: true,
96
+ })
97
+ const receivedFieldsToCheck = valueToSchema(findByIdResponse.record.fields, schemaToCompare)
98
+
99
+ const diff = getNotMatchingSubFields(receivedFieldsToCheck, sentFieldsToCheck)
100
+
101
+ await this.assert(() => !diff, 'Returned fields match updated fields', {
102
+ difference: diff,
103
+ sentFields: sentFieldsToCheck,
104
+ receivedFields: receivedFieldsToCheck,
105
+ })
106
+ }
107
+ }
108
+
109
+ async generateConfig(): Promise<DataCollectionUpdateTestConfig> {
110
+ let id = (
111
+ this.environment.state[this.dataCollectionKey] as {
112
+ createdRecordId: string
113
+ }
114
+ )?.createdRecordId
115
+
116
+ if (!id) {
117
+ if (!this.dataCollection.list) {
118
+ throw new Error(
119
+ `Can't find a record to test update operation for ${this.dataCollectionKey}. List operation is not implemented for this data collection`,
120
+ )
121
+ }
122
+
123
+ const listResponse = await this.environment.client
124
+ .connection(this.environment.connectionId)
125
+ .dataCollection(this.dataCollectionKey)
126
+ .list({})
127
+
128
+ if (!listResponse?.records?.length) {
129
+ throw new Error(`No records found to test update for ${this.dataCollectionKey}`)
130
+ }
131
+
132
+ id = listResponse.records[0].id
133
+ }
134
+
135
+ const updateFieldsSchema = getDataCollectionUpdateFields(this.dataCollection)
136
+
137
+ if (!updateFieldsSchema?.properties) {
138
+ throw new Error('No fields schema found for data collection')
139
+ }
140
+
141
+ const fields = await this.generateFieldsWithLLM(updateFieldsSchema)
142
+
143
+ return {
144
+ input: {
145
+ id,
146
+ fields,
147
+ },
148
+ }
149
+ }
150
+
151
+ protected async fixTestCase({
152
+ config,
153
+ error,
154
+ }: {
155
+ config: DataCollectionUpdateTestConfig
156
+ error: any
157
+ }): Promise<DataCollectionUpdateTestConfig> {
158
+ const errorData = extractIntegrationAppErrorData(error)
159
+
160
+ const updateFieldsSchema = getDataCollectionUpdateFields(this.dataCollection)
161
+
162
+ const examples = await this.getExampleRecordsForSchema(updateFieldsSchema)
163
+
164
+ const prompt = `I'm trying to update a data record in a data collection with the following fields schema:
165
+
166
+ ${JSON.stringify(updateFieldsSchema, null, 2)}
167
+
168
+ I tried to update a record with ID "${config.input?.id}" and these fields:
169
+
170
+ ${JSON.stringify(config.input?.fields, null, 2)}
171
+
172
+ But got this error:
173
+
174
+ ${JSON.stringify(errorData)}
175
+
176
+ ${this.getExampleRecordsPrompt(examples)}
177
+
178
+ Please analyze the error and provide:
179
+ 1. A fixed fields object that would work (as a JSON object)
180
+ 2. A short explanation of what was wrong with the original fields
181
+
182
+ Format your response as a JSON object with two fields:
183
+ {
184
+ "explanation": "short explanation of what was wrong and how you fixed it",
185
+ "fields": { ... fixed fields ... }
186
+ }.
187
+
188
+ Only return the JSON object, no other text or wrapping (like \`\`\`json or \`\`\`).`
189
+
190
+ const response = await this.environment.llm.complete({
191
+ prompt,
192
+ maxTokens: 10000,
193
+ })
194
+
195
+ const result = JSON.parse(response.trim())
196
+
197
+ console.warn(chalk.bold.yellow('[auto-fix]'), `${this.path}:`, result.explanation)
198
+
199
+ return {
200
+ input: {
201
+ id: config.input.id,
202
+ fields: result.fields,
203
+ },
204
+ }
205
+ }
206
+
207
+ private async generateFieldsWithLLM(schema: any): Promise<Record<string, any>> {
208
+ const examples = await this.getExampleRecordsForSchema(schema)
209
+
210
+ const prompt = `Generate a valid JSON object that matches this JSONSchema. Return only the JSON object, no other text.
211
+
212
+ ${this.getExampleRecordsPrompt(examples)}
213
+
214
+ JSONSchema:
215
+ ${JSON.stringify(schema, null, 2)}.
216
+
217
+ Do not populate fields with null or empty strings, unless the schema explicitly requires it.
218
+
219
+ Only return the JSON object, no other text or wrapping (like \`\`\`json or \`\`\`).`
220
+
221
+ const response = await this.environment.llm.complete({
222
+ prompt,
223
+ maxTokens: 10000,
224
+ })
225
+
226
+ const fields = JSON.parse(response.trim())
227
+ return fields
228
+ }
229
+
230
+ private getExampleRecordsPrompt(examples: Record<string, ExampleRecord[]>): string {
231
+ const examplesText = Object.entries(examples)
232
+ .map(([key, records]) => {
233
+ return `Example records from collection "${key}":\n${JSON.stringify(records, null, 2)}`
234
+ })
235
+ .join('\n\n')
236
+
237
+ return `When a field has a "referenceCollection" property, its value should be an ID of a record from that collection. Here are some example records you can use as references:
238
+
239
+ ${examplesText}.
240
+
241
+ If you don't have an example for a given collection, leave the field empty instead of coming up with a fake record id.`
242
+ }
243
+
244
+ private async findReferenceCollections(schema: any): Promise<ReferenceCollection[]> {
245
+ const references = new Set<string>()
246
+ const result: ReferenceCollection[] = []
247
+
248
+ walkSchema(schema, (obj) => {
249
+ if (obj.referenceCollection) {
250
+ const key = obj.referenceCollection.key
251
+ const parameters = obj.referenceCollection.parameters
252
+ const refKey = `${key}:${JSON.stringify(parameters || {})}`
253
+
254
+ if (!references.has(refKey)) {
255
+ references.add(refKey)
256
+ result.push({ key, parameters })
257
+ }
258
+ }
259
+ return obj
260
+ })
261
+
262
+ return result
263
+ }
264
+
265
+ private async fetchExampleRecords(collection: ReferenceCollection): Promise<ExampleRecord[]> {
266
+ const response = await this.environment.client
267
+ .connection(this.environment.connectionId)
268
+ .dataCollection(collection.key)
269
+ .list({
270
+ parameters: collection.parameters,
271
+ })
272
+ return response.records.map((r) => ({ id: r.id, fields: r.fields }))
273
+ }
274
+
275
+ private async getExampleRecordsForSchema(schema: any): Promise<Record<string, ExampleRecord[]>> {
276
+ const collections = await this.findReferenceCollections(schema)
277
+ const result: Record<string, ExampleRecord[]> = {}
278
+ for (const collection of collections) {
279
+ const locationPath = makeDataLocationPath(collection)
280
+ result[locationPath] = await this.fetchExampleRecords(collection)
281
+ }
282
+ return result
283
+ }
284
+ }
@@ -0,0 +1,19 @@
1
+ import jwt from 'jsonwebtoken'
2
+
3
+ export async function generateAccessToken(key: string, secret: string): Promise<string> {
4
+ try {
5
+ return jwt.sign(
6
+ {
7
+ name: 'Membrane CLI',
8
+ isAdmin: true,
9
+ exp: Math.floor(Date.now() / 1000) + 3600,
10
+ iss: key,
11
+ },
12
+ secret,
13
+ { algorithm: 'HS512' },
14
+ )
15
+ } catch (error) {
16
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
17
+ throw new Error(`Failed to generate access token: ${errorMessage}`)
18
+ }
19
+ }
@@ -0,0 +1,27 @@
1
+ import * as path from 'node:path'
2
+
3
+ export const MEMBRANE_DIR_NAME = 'membrane'
4
+
5
+ export const CONFIG_FILE_NAME = 'membrane.config.yml'
6
+
7
+ export function getPaths() {
8
+ const baseDir = process.cwd()
9
+
10
+ const membraneDirPath = path.join(baseDir, MEMBRANE_DIR_NAME)
11
+ const configPath = path.join(baseDir, CONFIG_FILE_NAME)
12
+
13
+ const payloadDirPath = membraneDirPath
14
+ const workspaceDataFilePath = path.join(payloadDirPath, 'workspace.yaml')
15
+
16
+ return {
17
+ membraneDirPath,
18
+ configPath,
19
+ payloadDirPath,
20
+ workspaceDataFilePath,
21
+ }
22
+ }
23
+
24
+ export function getBasePath() {
25
+ const paths = getPaths()
26
+ return paths.membraneDirPath
27
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Returns a subset of subFields that have values that do not match the values in allFields.
3
+ * For nested objects and arrays, returns only items that do not match.
4
+ */
5
+ export function getNotMatchingSubFields(
6
+ allFields: Record<string, any>,
7
+ subFields: Record<string, any>,
8
+ ): Record<string, any> | null {
9
+ const result: Record<string, any> = {}
10
+ for (const key in subFields) {
11
+ if (!(key in allFields)) {
12
+ result[key] = subFields[key]
13
+ continue
14
+ }
15
+ const origVal = allFields[key]
16
+ const subVal = subFields[key]
17
+ if (origVal && subVal && typeof origVal === 'object' && typeof subVal === 'object') {
18
+ if (Array.isArray(origVal) && Array.isArray(subVal)) {
19
+ const notMatchingValues = subVal.filter(
20
+ (subItem) =>
21
+ !origVal.some((origItem) => {
22
+ if (
23
+ typeof origItem === 'object' &&
24
+ typeof subItem === 'object' &&
25
+ origItem !== null &&
26
+ subItem !== null &&
27
+ !Array.isArray(origItem) &&
28
+ !Array.isArray(subItem)
29
+ ) {
30
+ for (const subKey in subItem) {
31
+ if (!(subKey in origItem) || !softCompare(origItem[subKey], subItem[subKey])) {
32
+ return false
33
+ }
34
+ }
35
+ return true
36
+ } else {
37
+ return JSON.stringify(origItem) === JSON.stringify(subItem)
38
+ }
39
+ }),
40
+ )
41
+ if (notMatchingValues.length > 0) {
42
+ result[key] = notMatchingValues
43
+ }
44
+ } else if (!Array.isArray(origVal) && !Array.isArray(subVal)) {
45
+ const subResult = getNotMatchingSubFields(origVal, subVal)
46
+ if (subResult !== null) {
47
+ result[key] = subResult
48
+ }
49
+ } else {
50
+ result[key] = subVal
51
+ }
52
+ continue
53
+ }
54
+ if (!softCompare(origVal, subVal)) {
55
+ result[key] = subVal
56
+ }
57
+ }
58
+ return Object.keys(result).length === 0 ? null : result
59
+ }
60
+
61
+ // it shouldnt fail because one is string and other is number in the account create etc.
62
+ // more details in the match test operation.
63
+
64
+ /**
65
+ * Compare two values in a way that is forgiving for typical API quirks.
66
+ */
67
+ export function softCompare(a: any, b: any) {
68
+ if (a == b) {
69
+ return true
70
+ }
71
+
72
+ if (a?.toString?.() === b?.toString?.()) {
73
+ return true
74
+ }
75
+
76
+ const dateA = new Date(a)
77
+ const dateB = new Date(b)
78
+ if (!isNaN(dateA.getTime()) && !isNaN(dateB.getTime())) {
79
+ return dateA.getTime() === dateB.getTime()
80
+ }
81
+
82
+ return false
83
+ }
@@ -0,0 +1,106 @@
1
+ import chalk from 'chalk'
2
+
3
+ type LogLevel = 'info' | 'success' | 'warning' | 'error' | 'debug'
4
+
5
+ interface LogOptions {
6
+ prefix?: string
7
+ suffix?: string
8
+ timestamp?: boolean
9
+ error?: string
10
+ color?: string
11
+ }
12
+
13
+ const LOG_ICONS = {
14
+ info: '📝',
15
+ success: '✨',
16
+ warning: '⚠️',
17
+ error: '❌',
18
+ debug: '🔍',
19
+ }
20
+
21
+ const LOG_STYLES = {
22
+ info: chalk.blue,
23
+ success: chalk.green,
24
+ warning: chalk.yellow,
25
+ error: chalk.red,
26
+ debug: chalk.gray,
27
+ }
28
+
29
+ export class Logger {
30
+ private static formatMessage(message: string, level: LogLevel, options: LogOptions = {}) {
31
+ const timestamp = options.timestamp ? `${chalk.gray(new Date().toISOString())} ` : ''
32
+ const prefix = options.prefix ? `${chalk.gray(options.prefix)} ` : ''
33
+ const suffix = options.suffix ? ` ${chalk.gray(options.suffix)}` : ''
34
+ const icon = LOG_ICONS[level]
35
+ const style = LOG_STYLES[level]
36
+
37
+ return `${timestamp}${icon} ${prefix}${style(message)}${suffix}`
38
+ }
39
+
40
+ static info(message: string, options?: LogOptions) {
41
+ let chalkMethod = chalk.blue
42
+
43
+ if (options?.color) {
44
+ chalkMethod = chalk[options.color.toLowerCase()] || chalk.blue
45
+ }
46
+
47
+ if (options?.timestamp) {
48
+ const timestamp = new Date().toLocaleTimeString()
49
+ console.debug(`${chalk.gray(`[${timestamp}]`)} ${chalkMethod(message)}`)
50
+ } else {
51
+ console.debug(chalkMethod(message))
52
+ }
53
+ }
54
+
55
+ static group(_message: string, _options?: LogOptions) {}
56
+
57
+ static groupEnd() {}
58
+ static newLine() {}
59
+
60
+ static success(message: string, options?: LogOptions) {
61
+ console.debug(this.formatMessage(message, 'success', options))
62
+ }
63
+
64
+ static warning(message: string, options?: LogOptions) {
65
+ console.debug(this.formatMessage(message, 'warning', options))
66
+ }
67
+
68
+ static error(message: string, options?: LogOptions) {
69
+ console.error(this.formatMessage(message, 'error', options))
70
+ }
71
+
72
+ static debug(message: string, options?: LogOptions) {
73
+ if (options?.prefix) {
74
+ console.debug(chalk.gray(`[${options.prefix}] ${message}`), options.error ? `\n${chalk.red(options.error)}` : '')
75
+ } else {
76
+ console.debug(chalk.gray(message), options?.error ? `\n${chalk.red(options.error)}` : '')
77
+ }
78
+ }
79
+
80
+ static step(_stepNumber: number, _message: string) {}
81
+
82
+ static header(message: string) {
83
+ console.debug()
84
+ console.debug(chalk.bold.cyan(`▶ ${message}`))
85
+ console.debug()
86
+ }
87
+
88
+ static table(data: Record<string, any>[], columns?: string[]) {
89
+ if (data.length === 0) {
90
+ return
91
+ }
92
+
93
+ const columnsToShow = columns || Object.keys(data[0])
94
+ const _formattedData = data.map((item) => {
95
+ const formatted: Record<string, any> = {}
96
+ columnsToShow.forEach((col) => {
97
+ formatted[col] = item[col]
98
+ })
99
+ return formatted
100
+ })
101
+ // eslint-disable-next-line no-console
102
+ console.table(_formattedData, columnsToShow)
103
+ }
104
+
105
+ static divider() {}
106
+ }
@@ -0,0 +1,50 @@
1
+ import { faker } from '@faker-js/faker'
2
+ import template from 'lodash/template.js'
3
+ import templateSettings from 'lodash/templateSettings.js'
4
+
5
+ templateSettings.interpolate = /{{([\s\S]+?)}}/g
6
+
7
+ function processNode(node: any, state: any): any {
8
+ if (typeof node === 'string') {
9
+ const compiled = template(node)
10
+ const templateData = {
11
+ state,
12
+ random: {
13
+ number: () => faker.number.int(),
14
+ alphaNumeric: (count: number) => faker.string.alphanumeric(count),
15
+ },
16
+ faker: {
17
+ company: {
18
+ name: () => faker.company.name(),
19
+ catchPhrase: () => faker.company.catchPhrase(),
20
+ },
21
+ internet: {
22
+ email: () => faker.internet.email(),
23
+ },
24
+ },
25
+ entries: state['journal-entries'],
26
+ orders: state['purchase-orders'] || state['sales-orders'],
27
+ bills: state['vendor-bills'],
28
+ }
29
+
30
+ const result = compiled(templateData)
31
+
32
+ if (node.includes('{{') && node.includes('}}')) {
33
+ console.debug(`[TEMPLATE] Input: ${node}`)
34
+ console.debug(`[TEMPLATE] Output: ${result}`)
35
+ }
36
+
37
+ return result
38
+ }
39
+ if (Array.isArray(node)) {
40
+ return node.map((item) => processNode(item, state))
41
+ }
42
+ if (typeof node === 'object' && node !== null) {
43
+ return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, processNode(value, state)]))
44
+ }
45
+ return node
46
+ }
47
+
48
+ export function handleTemplate(config: any, state: any) {
49
+ return processNode(config, state)
50
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "target": "ESNext",
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "types": ["node", "vitest/globals"],
8
+ "allowImportingTsExtensions": true,
9
+ "allowJs": true,
10
+ "strict": true,
11
+ "noEmit": true,
12
+ "skipLibCheck": true,
13
+ "esModuleInterop": true,
14
+ "resolveJsonModule": true,
15
+ "jsx": "react-jsx",
16
+ "outDir": "dist",
17
+ "rootDir": "src"
18
+ },
19
+ "include": ["src/**/*.ts", "src/**/*.tsx"],
20
+ "exclude": ["node_modules"]
21
+ }