@brickflow/cli 0.0.6 → 0.0.8

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.
@@ -0,0 +1,409 @@
1
+ import { parse as babelParse } from '@babel/parser'
2
+ import traverseModule from '@babel/traverse'
3
+ import bkt from '@babel/types'
4
+ const { isIdentifier, isMemberExpression, isStringLiteral, isTemplateLiteral } = bkt
5
+ import { compileTemplate, parse as parseSFC } from '@vue/compiler-sfc'
6
+ import { execFileSync } from 'child_process'
7
+ import { createHash } from 'crypto'
8
+ import { existsSync } from 'fs'
9
+ import { globSync } from 'glob'
10
+ import { join, resolve } from 'path'
11
+
12
+ const tTraverse = traverseModule.default || traverseModule
13
+ const IGNORED_GLOB_PATTERNS = [
14
+ '**/node_modules/**',
15
+ '**/.nuxt/**',
16
+ '**/dist/**',
17
+ '**/.output/**',
18
+ '**/coverage/**',
19
+ '**/public/**',
20
+ ]
21
+ const IGNORED_SOURCE_SEGMENTS = ['/node_modules/', '/.nuxt/', '/dist/', '/.output/', '/coverage/', '/public/']
22
+ const TRANSLATION_SAMPLE_GLOB =
23
+ '{packages/brick,apps/*}/{components/**/translate,pages-translate/*,layouts/**,global/*}/sample.json'
24
+
25
+ export function compileVueToJS(code, filePath) {
26
+ const { descriptor } = parseSFC(code)
27
+
28
+ let result = ''
29
+
30
+ if (descriptor.script?.content) {
31
+ result += `${descriptor.script.content}\n`
32
+ }
33
+
34
+ if (descriptor.scriptSetup?.content) {
35
+ result += `${descriptor.scriptSetup.content}\n`
36
+ }
37
+
38
+ if (descriptor.template?.content) {
39
+ const compiled = compileTemplate({
40
+ filename: filePath,
41
+ id: filePath,
42
+ source: descriptor.template.content,
43
+ })
44
+
45
+ result += compiled.code
46
+ }
47
+
48
+ return result
49
+ }
50
+
51
+ export function extractStrings(code, id) {
52
+ const ast = babelParse(code, {
53
+ plugins: getBabelPlugins(id),
54
+ sourceType: 'module',
55
+ })
56
+
57
+ const result = new Map()
58
+ const component = getComponentName(id)
59
+
60
+ tTraverse(ast, {
61
+ CallExpression(pathAst) {
62
+ if (!isTCall(pathAst.node)) {
63
+ return
64
+ }
65
+
66
+ const arg = pathAst.node.arguments[0]
67
+ const text = getStaticText(arg)
68
+
69
+ if (text === null) {
70
+ return
71
+ }
72
+
73
+ const key = generateKey(text, component)
74
+
75
+ result.set(key, text)
76
+ },
77
+ })
78
+
79
+ return result
80
+ }
81
+
82
+ export function generateKey(text, component) {
83
+ const normalized = normalize(text)
84
+ const cased = toSnake(normalized).slice(0, 10)
85
+ const hash = shortHash(normalized)
86
+
87
+ return `${component}.${cased}_${hash}`
88
+ }
89
+
90
+ export function getComponentName(id) {
91
+ const normalizedId = id.replace(/[?#].*$/, '')
92
+
93
+ if (/\/pages\//.test(normalizedId)) {
94
+ const pagePath = normalizedId.split('/pages/')[1].replace(/\.\w+$/, '')
95
+
96
+ return pagePath
97
+ .split('/')
98
+ .filter(Boolean)
99
+ .map((segment) => toPageSegmentName(segment))
100
+ .join('_')
101
+ }
102
+
103
+ if (/\/layouts\//.test(normalizedId)) {
104
+ const layoutPath = normalizedId
105
+ .split('/layouts/')[1]
106
+ .replace(/\.\w+$/, '')
107
+ .replace(/\/index$/, '')
108
+ const segments = [getProjectName(normalizedId), 'layouts', ...layoutPath.split('/').filter(Boolean)]
109
+
110
+ return segments.map((segment) => toPascal(segment)).join('')
111
+ }
112
+
113
+ if (!normalizedId.endsWith('.vue')) {
114
+ return getScriptName(normalizedId)
115
+ }
116
+
117
+ const parts = normalizedId.split('/')
118
+ parts.pop()
119
+
120
+ const dirs = parts.slice(-2)
121
+
122
+ return dirs.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')
123
+ }
124
+
125
+ export function getProjectName(id) {
126
+ return getProjectRoot(id)?.split('/').pop() ?? 'project'
127
+ }
128
+
129
+ export function getProjectRoot(id) {
130
+ const normalizedId = id.replace(/[?#].*$/, '')
131
+ const brickMatch = normalizedId.match(/^(.*\/packages\/brick)(?:\/|$)/)
132
+
133
+ if (brickMatch) {
134
+ return brickMatch[1]
135
+ }
136
+
137
+ const appMatch = normalizedId.match(/^(.*\/apps\/[^/]+)(?:\/|$)/)
138
+
139
+ return appMatch?.[1] ?? null
140
+ }
141
+
142
+ export function getScriptName(id) {
143
+ const relativePath = id.split('/packages/')[1] ?? id.split('/apps/')[1] ?? id.replace(/^\//, '')
144
+
145
+ return toPageName(relativePath.replace(/\.\w+$/, '').replace(/\//g, '_'))
146
+ }
147
+
148
+ export function getTranslationPaths(id) {
149
+ if (isVueSubResourceId(id)) {
150
+ return null
151
+ }
152
+
153
+ const normalizedId = id.replace(/[?#].*$/, '')
154
+ if (isIgnoredSourcePath(normalizedId)) {
155
+ return null
156
+ }
157
+
158
+ if (normalizedId.endsWith('/book.vue')) {
159
+ return null
160
+ }
161
+
162
+ const isComponent =
163
+ normalizedId.endsWith('.vue') &&
164
+ (normalizedId.includes('/packages/brick/components/') ||
165
+ (normalizedId.includes('/apps/') && normalizedId.includes('/components/')))
166
+ const isPage = normalizedId.endsWith('.vue') && normalizedId.includes('/pages/')
167
+ const isLayout = normalizedId.endsWith('.vue') && normalizedId.includes('/layouts/')
168
+ const projectRoot = getProjectRoot(normalizedId)
169
+ const isScript =
170
+ !normalizedId.endsWith('.vue') &&
171
+ Boolean(projectRoot) &&
172
+ /\.(?:js|ts)$/.test(normalizedId) &&
173
+ !normalizedId.endsWith('.d.ts')
174
+
175
+ if (!isComponent && !isPage && !isLayout && !isScript) {
176
+ return null
177
+ }
178
+
179
+ if (isComponent) {
180
+ const componentDir = normalizedId.split('/').slice(0, -1).join('/')
181
+ const baseDir = join(componentDir, 'translate')
182
+
183
+ return {
184
+ baseDir,
185
+ isComponent,
186
+ isPage,
187
+ isScript,
188
+ samplePath: join(baseDir, 'sample.json'),
189
+ }
190
+ }
191
+
192
+ if (isPage) {
193
+ const pagesRoot = `${normalizedId.split('/pages/')[0]}/pages`
194
+ const rootDir = join(pagesRoot, '..', 'pages-translate')
195
+ const name = getComponentName(normalizedId)
196
+ const baseDir = join(rootDir, name)
197
+
198
+ return {
199
+ baseDir,
200
+ isComponent,
201
+ isPage,
202
+ isScript,
203
+ samplePath: join(baseDir, 'sample.json'),
204
+ }
205
+ }
206
+
207
+ if (isLayout) {
208
+ const layoutDir = normalizedId.split('/').slice(0, -1).join('/')
209
+ const fileName =
210
+ normalizedId
211
+ .split('/')
212
+ .pop()
213
+ ?.replace(/\.\w+$/, '') ?? ''
214
+ const baseDir = join(layoutDir, fileName)
215
+
216
+ return {
217
+ baseDir,
218
+ isComponent,
219
+ isLayout,
220
+ isPage,
221
+ isScript,
222
+ samplePath: join(baseDir, 'sample.json'),
223
+ }
224
+ }
225
+
226
+ const baseDir = join(projectRoot, 'global', getComponentName(normalizedId))
227
+
228
+ return {
229
+ baseDir,
230
+ isComponent,
231
+ isLayout,
232
+ isPage,
233
+ isScript,
234
+ samplePath: join(baseDir, 'sample.json'),
235
+ }
236
+ }
237
+
238
+ export function listWorkspaceFiles(workspaceRoot) {
239
+ try {
240
+ const output = execFileSync('git', ['ls-files', '-co', '--exclude-standard', '-z'], {
241
+ cwd: workspaceRoot,
242
+ encoding: 'utf-8',
243
+ })
244
+
245
+ return output
246
+ .split('\0')
247
+ .filter(Boolean)
248
+ .map((file) => resolve(workspaceRoot, file))
249
+ .filter((filePath) => existsSync(filePath))
250
+ } catch {
251
+ return globSync('**/*', {
252
+ absolute: true,
253
+ cwd: workspaceRoot,
254
+ ignore: IGNORED_GLOB_PATTERNS,
255
+ nodir: true,
256
+ })
257
+ }
258
+ }
259
+
260
+ export function listTranslationSamplePaths(workspaceRoot) {
261
+ return globSync(TRANSLATION_SAMPLE_GLOB, {
262
+ absolute: true,
263
+ cwd: workspaceRoot,
264
+ ignore: IGNORED_GLOB_PATTERNS,
265
+ }).sort()
266
+ }
267
+
268
+ export function listTranslationTargets(workspaceRoot) {
269
+ const samplePaths = listTranslationSamplePaths(workspaceRoot)
270
+ const sourceFiles = listWorkspaceFiles(workspaceRoot).filter(
271
+ (filePath) => /\.(?:js|ts|vue)$/.test(filePath) && !filePath.endsWith('.d.ts'),
272
+ )
273
+ const sampleToSource = new Map()
274
+
275
+ for (const sourceFilePath of sourceFiles) {
276
+ const samplePath = getTranslationPaths(sourceFilePath)?.samplePath
277
+
278
+ if (samplePath && existsSync(samplePath) && !sampleToSource.has(samplePath)) {
279
+ sampleToSource.set(samplePath, sourceFilePath)
280
+ }
281
+ }
282
+
283
+ return samplePaths.map((samplePath) => ({
284
+ samplePath,
285
+ sourceFilePath: sampleToSource.get(samplePath),
286
+ }))
287
+ }
288
+
289
+ export function normalize(input) {
290
+ return input.trim().toLowerCase().replace(/\s+/g, ' ')
291
+ }
292
+
293
+ export function shortHash(input) {
294
+ return createHash('md5').update(input).digest('hex').slice(0, 4)
295
+ }
296
+
297
+ export function sortJsonValue(value) {
298
+ if (Array.isArray(value)) {
299
+ return value.map((item) => sortJsonValue(item))
300
+ }
301
+
302
+ if (value && typeof value === 'object') {
303
+ return Object.fromEntries(
304
+ Object.entries(value)
305
+ .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
306
+ .map(([key, item]) => [key, sortJsonValue(item)]),
307
+ )
308
+ }
309
+
310
+ return value
311
+ }
312
+
313
+ export function sortObjectKeys(value) {
314
+ return Object.fromEntries(Object.entries(value).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)))
315
+ }
316
+
317
+ export function stringifySortedJson(value) {
318
+ return `${JSON.stringify(sortJsonValue(value), null, 2)}\n`
319
+ }
320
+
321
+ export function toPageName(input) {
322
+ return input
323
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
324
+ .replace(/[-.\s]+/g, '_')
325
+ .replace(/[^\w$]/g, '')
326
+ .replace(/_+/g, '_')
327
+ .replace(/^_+|_+$/g, '')
328
+ .toLowerCase()
329
+ }
330
+
331
+ export function toSnake(input) {
332
+ return input
333
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
334
+ .replace(/[-.\s]+/g, '_')
335
+ .replace(/\W/g, '')
336
+ .replace(/_+/g, '_')
337
+ .replace(/^_+|_+$/g, '')
338
+ .toLowerCase()
339
+ }
340
+
341
+ function getBabelPlugins(id) {
342
+ const normalizedId = id.replace(/[?#].*$/, '')
343
+
344
+ if (/\.(?:jsx|tsx)$/.test(normalizedId)) {
345
+ return ['typescript', 'jsx']
346
+ }
347
+
348
+ return ['typescript']
349
+ }
350
+
351
+ function getStaticText(node) {
352
+ if (!node) {
353
+ return null
354
+ }
355
+
356
+ if (isStringLiteral(node)) {
357
+ return node.value
358
+ }
359
+
360
+ if (isTemplateLiteral(node) && node.expressions.length === 0) {
361
+ return node.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw).join('')
362
+ }
363
+
364
+ return null
365
+ }
366
+
367
+ function isIgnoredSourcePath(id) {
368
+ return IGNORED_SOURCE_SEGMENTS.some((segment) => id.includes(segment))
369
+ }
370
+
371
+ function isTCall(node) {
372
+ const callee = node.callee
373
+
374
+ if (isIdentifier(callee) && callee.name === 't') {
375
+ return true
376
+ }
377
+
378
+ if (isMemberExpression(callee) && isIdentifier(callee.property) && callee.property.name === 't') {
379
+ if (isIdentifier(callee.object, { name: 'currentI18n' })) {
380
+ return false
381
+ }
382
+
383
+ return true
384
+ }
385
+
386
+ return false
387
+ }
388
+
389
+ function isVueSubResourceId(id) {
390
+ return /[?&]vue&type=/.test(id)
391
+ }
392
+
393
+ function toPageSegmentName(segment) {
394
+ const dynamicMatch = segment.match(/^\[([^\]]+)\]$/)
395
+
396
+ if (dynamicMatch) {
397
+ return `$${dynamicMatch[1]}`
398
+ }
399
+
400
+ return toSnake(segment)
401
+ }
402
+
403
+ function toPascal(input) {
404
+ return toSnake(input)
405
+ .split('_')
406
+ .filter(Boolean)
407
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
408
+ .join('')
409
+ }
@@ -0,0 +1,3 @@
1
+ import { runAiContextCli } from '../translate/ai-context.js'
2
+
3
+ await runAiContextCli(process.argv.slice(3))
@@ -0,0 +1,25 @@
1
+ const args = process.argv.slice(3)
2
+
3
+ if (args.includes('--help') || args.includes('-h')) {
4
+ printHelp()
5
+ process.exit(0)
6
+ }
7
+
8
+ if (args.length > 0) {
9
+ printHelp()
10
+ process.exit(1)
11
+ }
12
+
13
+ await import('../translate/sync.js')
14
+
15
+ function printHelp() {
16
+ console.log(`brick translate-sync
17
+
18
+ Usage:
19
+ brick translate-sync
20
+
21
+ Notes:
22
+ Scans source files and regenerates translation sample.json files
23
+ Removes obsolete generated translation directories
24
+ Verifies and normalizes translation JSON key ordering`)
25
+ }
@@ -0,0 +1,127 @@
1
+ import { readdirSync, statSync, writeFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ const args = process.argv.slice(3)
5
+
6
+ if (args.includes('--help') || args.includes('-h')) {
7
+ printHelp()
8
+ process.exit(0)
9
+ }
10
+
11
+ const options = parseArgs(args)
12
+
13
+ if (!options.path || !options.typeName || !options.outputFile || !options.replacePattern) {
14
+ printHelp()
15
+ process.exit(1)
16
+ }
17
+
18
+ const inputDir = path.resolve(process.cwd(), options.path)
19
+ const outputFile = path.resolve(process.cwd(), options.outputFile)
20
+ const replacePattern = parseReplacePattern(options.replacePattern)
21
+ const entries = readdirSync(inputDir)
22
+ .filter((entry) => statSync(path.join(inputDir, entry)).isFile())
23
+ .sort((first, second) => first.localeCompare(second))
24
+
25
+ const typeValues = new Set(entries.map((entry) => applyReplacePattern(entry, replacePattern)).filter(Boolean))
26
+
27
+ if (typeValues.size === 0) {
28
+ throw new Error(`No type values generated from: ${inputDir}`)
29
+ }
30
+
31
+ const declaration = `declare type ${options.typeName} = ${[...typeValues].map((value) => `'${value}'`).join(' | ')}\n`
32
+ writeFileSync(outputFile, declaration, 'utf8')
33
+
34
+ console.log(`✅ Types generated for ${options.typeName}`)
35
+ console.log(` input: ${inputDir}`)
36
+ console.log(` output: ${outputFile}`)
37
+
38
+ function applyReplacePattern(value, pattern) {
39
+ if (pattern instanceof RegExp) {
40
+ return value.replace(pattern, '')
41
+ }
42
+
43
+ return value.replaceAll(pattern, '')
44
+ }
45
+
46
+ function parseArgs(rawArgs) {
47
+ const parsedOptions = {
48
+ outputFile: null,
49
+ path: null,
50
+ replacePattern: null,
51
+ typeName: null,
52
+ }
53
+ const positional = []
54
+
55
+ for (let index = 0; index < rawArgs.length; index += 1) {
56
+ const value = rawArgs[index]
57
+
58
+ if (value === '--path') {
59
+ parsedOptions.path = rawArgs[index + 1] ?? null
60
+ index += 1
61
+ continue
62
+ }
63
+
64
+ if (value === '--type-name') {
65
+ parsedOptions.typeName = rawArgs[index + 1] ?? null
66
+ index += 1
67
+ continue
68
+ }
69
+
70
+ if (value === '--output-file') {
71
+ parsedOptions.outputFile = rawArgs[index + 1] ?? null
72
+ index += 1
73
+ continue
74
+ }
75
+
76
+ if (value === '--replace-pattern') {
77
+ parsedOptions.replacePattern = rawArgs[index + 1] ?? null
78
+ index += 1
79
+ continue
80
+ }
81
+
82
+ positional.push(value)
83
+ }
84
+
85
+ if (!parsedOptions.path) {
86
+ parsedOptions.path = positional[0] ?? null
87
+ }
88
+
89
+ if (!parsedOptions.typeName) {
90
+ parsedOptions.typeName = positional[1] ?? null
91
+ }
92
+
93
+ if (!parsedOptions.outputFile) {
94
+ parsedOptions.outputFile = positional[2] ?? null
95
+ }
96
+
97
+ if (!parsedOptions.replacePattern) {
98
+ parsedOptions.replacePattern = positional[3] ?? null
99
+ }
100
+
101
+ return parsedOptions
102
+ }
103
+
104
+ function parseReplacePattern(value) {
105
+ const match = value.match(/^\/([\s\S]*)\/([dgimsuvy]*)$/)
106
+
107
+ if (match) {
108
+ return new RegExp(match[1], match[2])
109
+ }
110
+
111
+ return value
112
+ }
113
+
114
+ function printHelp() {
115
+ console.log(`brick types <path> <typeName> <outputFile> <replacePattern>
116
+
117
+ Usage:
118
+ brick types ./path/to/files IconName ./types/icon.d.ts .svg
119
+ brick types ./path/to/files IconName ./types/icon.d.ts /\\.svg$/
120
+ brick types --path ./path/to/files --type-name IconName --output-file ./types/icon.d.ts --replace-pattern /\\.svg$/
121
+
122
+ Notes:
123
+ path: directory with source files
124
+ typeName: generated TypeScript type name
125
+ outputFile: file to write declaration into
126
+ replacePattern: string or regex literal used in replace(..., '') for each filename`)
127
+ }