@brickflow/cli 0.0.6 → 0.0.7

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,378 @@
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
+
23
+ export function compileVueToJS(code, filePath) {
24
+ const { descriptor } = parseSFC(code)
25
+
26
+ let result = ''
27
+
28
+ if (descriptor.script?.content) {
29
+ result += `${descriptor.script.content}\n`
30
+ }
31
+
32
+ if (descriptor.scriptSetup?.content) {
33
+ result += `${descriptor.scriptSetup.content}\n`
34
+ }
35
+
36
+ if (descriptor.template?.content) {
37
+ const compiled = compileTemplate({
38
+ filename: filePath,
39
+ id: filePath,
40
+ source: descriptor.template.content,
41
+ })
42
+
43
+ result += compiled.code
44
+ }
45
+
46
+ return result
47
+ }
48
+
49
+ export function extractStrings(code, id) {
50
+ const ast = babelParse(code, {
51
+ plugins: getBabelPlugins(id),
52
+ sourceType: 'module',
53
+ })
54
+
55
+ const result = new Map()
56
+ const component = getComponentName(id)
57
+
58
+ tTraverse(ast, {
59
+ CallExpression(pathAst) {
60
+ if (!isTCall(pathAst.node)) {
61
+ return
62
+ }
63
+
64
+ const arg = pathAst.node.arguments[0]
65
+ const text = getStaticText(arg)
66
+
67
+ if (text === null) {
68
+ return
69
+ }
70
+
71
+ const key = generateKey(text, component)
72
+
73
+ result.set(key, text)
74
+ },
75
+ })
76
+
77
+ return result
78
+ }
79
+
80
+ export function generateKey(text, component) {
81
+ const normalized = normalize(text)
82
+ const cased = toSnake(normalized).slice(0, 10)
83
+ const hash = shortHash(normalized)
84
+
85
+ return `${component}.${cased}_${hash}`
86
+ }
87
+
88
+ export function getComponentName(id) {
89
+ const normalizedId = id.replace(/[?#].*$/, '')
90
+
91
+ if (/\/pages\//.test(normalizedId)) {
92
+ const pagePath = normalizedId.split('/pages/')[1].replace(/\.\w+$/, '')
93
+
94
+ return pagePath
95
+ .split('/')
96
+ .filter(Boolean)
97
+ .map((segment) => toPageSegmentName(segment))
98
+ .join('_')
99
+ }
100
+
101
+ if (/\/layouts\//.test(normalizedId)) {
102
+ const layoutPath = normalizedId
103
+ .split('/layouts/')[1]
104
+ .replace(/\.\w+$/, '')
105
+ .replace(/\/index$/, '')
106
+ const segments = [getProjectName(normalizedId), 'layouts', ...layoutPath.split('/').filter(Boolean)]
107
+
108
+ return segments.map((segment) => toPascal(segment)).join('')
109
+ }
110
+
111
+ if (!normalizedId.endsWith('.vue')) {
112
+ return getScriptName(normalizedId)
113
+ }
114
+
115
+ const parts = normalizedId.split('/')
116
+ parts.pop()
117
+
118
+ const dirs = parts.slice(-2)
119
+
120
+ return dirs.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')
121
+ }
122
+
123
+ export function getProjectName(id) {
124
+ return getProjectRoot(id)?.split('/').pop() ?? 'project'
125
+ }
126
+
127
+ export function getProjectRoot(id) {
128
+ const normalizedId = id.replace(/[?#].*$/, '')
129
+ const brickMatch = normalizedId.match(/^(.*\/packages\/brick)(?:\/|$)/)
130
+
131
+ if (brickMatch) {
132
+ return brickMatch[1]
133
+ }
134
+
135
+ const appMatch = normalizedId.match(/^(.*\/apps\/[^/]+)(?:\/|$)/)
136
+
137
+ return appMatch?.[1] ?? null
138
+ }
139
+
140
+ export function getScriptName(id) {
141
+ const relativePath = id.split('/packages/')[1] ?? id.split('/apps/')[1] ?? id.replace(/^\//, '')
142
+
143
+ return toPageName(relativePath.replace(/\.\w+$/, '').replace(/\//g, '_'))
144
+ }
145
+
146
+ export function getTranslationPaths(id) {
147
+ if (isVueSubResourceId(id)) {
148
+ return null
149
+ }
150
+
151
+ const normalizedId = id.replace(/[?#].*$/, '')
152
+ if (isIgnoredSourcePath(normalizedId)) {
153
+ return null
154
+ }
155
+
156
+ if (normalizedId.endsWith('/book.vue')) {
157
+ return null
158
+ }
159
+
160
+ const isComponent =
161
+ normalizedId.endsWith('.vue') &&
162
+ (normalizedId.includes('/packages/brick/components/') ||
163
+ (normalizedId.includes('/apps/') && normalizedId.includes('/components/')))
164
+ const isPage = normalizedId.endsWith('.vue') && normalizedId.includes('/pages/')
165
+ const isLayout = normalizedId.endsWith('.vue') && normalizedId.includes('/layouts/')
166
+ const projectRoot = getProjectRoot(normalizedId)
167
+ const isScript =
168
+ !normalizedId.endsWith('.vue') &&
169
+ Boolean(projectRoot) &&
170
+ /\.(?:js|ts)$/.test(normalizedId) &&
171
+ !normalizedId.endsWith('.d.ts')
172
+
173
+ if (!isComponent && !isPage && !isLayout && !isScript) {
174
+ return null
175
+ }
176
+
177
+ if (isComponent) {
178
+ const componentDir = normalizedId.split('/').slice(0, -1).join('/')
179
+ const baseDir = join(componentDir, 'translate')
180
+
181
+ return {
182
+ baseDir,
183
+ isComponent,
184
+ isPage,
185
+ isScript,
186
+ samplePath: join(baseDir, 'sample.json'),
187
+ }
188
+ }
189
+
190
+ if (isPage) {
191
+ const pagesRoot = `${normalizedId.split('/pages/')[0]}/pages`
192
+ const rootDir = join(pagesRoot, '..', 'pages-translate')
193
+ const name = getComponentName(normalizedId)
194
+ const baseDir = join(rootDir, name)
195
+
196
+ return {
197
+ baseDir,
198
+ isComponent,
199
+ isPage,
200
+ isScript,
201
+ samplePath: join(baseDir, 'sample.json'),
202
+ }
203
+ }
204
+
205
+ if (isLayout) {
206
+ const layoutDir = normalizedId.split('/').slice(0, -1).join('/')
207
+ const fileName =
208
+ normalizedId
209
+ .split('/')
210
+ .pop()
211
+ ?.replace(/\.\w+$/, '') ?? ''
212
+ const baseDir = join(layoutDir, fileName)
213
+
214
+ return {
215
+ baseDir,
216
+ isComponent,
217
+ isLayout,
218
+ isPage,
219
+ isScript,
220
+ samplePath: join(baseDir, 'sample.json'),
221
+ }
222
+ }
223
+
224
+ const baseDir = join(projectRoot, 'global', getComponentName(normalizedId))
225
+
226
+ return {
227
+ baseDir,
228
+ isComponent,
229
+ isLayout,
230
+ isPage,
231
+ isScript,
232
+ samplePath: join(baseDir, 'sample.json'),
233
+ }
234
+ }
235
+
236
+ export function listWorkspaceFiles(workspaceRoot) {
237
+ try {
238
+ const output = execFileSync('git', ['ls-files', '-co', '--exclude-standard', '-z'], {
239
+ cwd: workspaceRoot,
240
+ encoding: 'utf-8',
241
+ })
242
+
243
+ return output
244
+ .split('\0')
245
+ .filter(Boolean)
246
+ .map((file) => resolve(workspaceRoot, file))
247
+ .filter((filePath) => existsSync(filePath))
248
+ } catch {
249
+ return globSync('**/*', {
250
+ absolute: true,
251
+ cwd: workspaceRoot,
252
+ ignore: IGNORED_GLOB_PATTERNS,
253
+ nodir: true,
254
+ })
255
+ }
256
+ }
257
+
258
+ export function normalize(input) {
259
+ return input.trim().toLowerCase().replace(/\s+/g, ' ')
260
+ }
261
+
262
+ export function shortHash(input) {
263
+ return createHash('md5').update(input).digest('hex').slice(0, 4)
264
+ }
265
+
266
+ export function sortJsonValue(value) {
267
+ if (Array.isArray(value)) {
268
+ return value.map((item) => sortJsonValue(item))
269
+ }
270
+
271
+ if (value && typeof value === 'object') {
272
+ return Object.fromEntries(
273
+ Object.entries(value)
274
+ .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
275
+ .map(([key, item]) => [key, sortJsonValue(item)]),
276
+ )
277
+ }
278
+
279
+ return value
280
+ }
281
+
282
+ export function sortObjectKeys(value) {
283
+ return Object.fromEntries(Object.entries(value).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)))
284
+ }
285
+
286
+ export function stringifySortedJson(value) {
287
+ return `${JSON.stringify(sortJsonValue(value), null, 2)}\n`
288
+ }
289
+
290
+ export function toPageName(input) {
291
+ return input
292
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
293
+ .replace(/[-.\s]+/g, '_')
294
+ .replace(/[^\w$]/g, '')
295
+ .replace(/_+/g, '_')
296
+ .replace(/^_+|_+$/g, '')
297
+ .toLowerCase()
298
+ }
299
+
300
+ export function toSnake(input) {
301
+ return input
302
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
303
+ .replace(/[-.\s]+/g, '_')
304
+ .replace(/\W/g, '')
305
+ .replace(/_+/g, '_')
306
+ .replace(/^_+|_+$/g, '')
307
+ .toLowerCase()
308
+ }
309
+
310
+ function getBabelPlugins(id) {
311
+ const normalizedId = id.replace(/[?#].*$/, '')
312
+
313
+ if (/\.(?:jsx|tsx)$/.test(normalizedId)) {
314
+ return ['typescript', 'jsx']
315
+ }
316
+
317
+ return ['typescript']
318
+ }
319
+
320
+ function getStaticText(node) {
321
+ if (!node) {
322
+ return null
323
+ }
324
+
325
+ if (isStringLiteral(node)) {
326
+ return node.value
327
+ }
328
+
329
+ if (isTemplateLiteral(node) && node.expressions.length === 0) {
330
+ return node.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw).join('')
331
+ }
332
+
333
+ return null
334
+ }
335
+
336
+ function isIgnoredSourcePath(id) {
337
+ return IGNORED_SOURCE_SEGMENTS.some((segment) => id.includes(segment))
338
+ }
339
+
340
+ function isTCall(node) {
341
+ const callee = node.callee
342
+
343
+ if (isIdentifier(callee) && callee.name === 't') {
344
+ return true
345
+ }
346
+
347
+ if (isMemberExpression(callee) && isIdentifier(callee.property) && callee.property.name === 't') {
348
+ if (isIdentifier(callee.object, { name: 'currentI18n' })) {
349
+ return false
350
+ }
351
+
352
+ return true
353
+ }
354
+
355
+ return false
356
+ }
357
+
358
+ function isVueSubResourceId(id) {
359
+ return /[?&]vue&type=/.test(id)
360
+ }
361
+
362
+ function toPageSegmentName(segment) {
363
+ const dynamicMatch = segment.match(/^\[([^\]]+)\]$/)
364
+
365
+ if (dynamicMatch) {
366
+ return `$${dynamicMatch[1]}`
367
+ }
368
+
369
+ return toSnake(segment)
370
+ }
371
+
372
+ function toPascal(input) {
373
+ return toSnake(input)
374
+ .split('_')
375
+ .filter(Boolean)
376
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
377
+ .join('')
378
+ }
@@ -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
+ }