@lyda/kilo-ui 0.1.1 → 0.1.4

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.
package/README.md CHANGED
@@ -13,11 +13,12 @@ After you **publish** this project to npm, install the **exact** `name` from its
13
13
 
14
14
  ```bash
15
15
  npm install -D @lyda/kilo-ui
16
- npx kilo-ui init
17
16
  npx kilo-ui add data-table
18
17
  ```
19
18
 
20
- If the package was published with an installer hook, `npm install -D ...` will run `kilo-ui init` automatically once. If you use `--ignore-scripts`, run `npx kilo-ui init` manually.
19
+ **First install in a new project:** the package **postinstall** runs `kilo-ui init` once (unless `kilo.config.ts` already exists). That creates **`kilo.config.ts` in your app root** (next to `package.json`) — not inside `node_modules`. Edit it for **aliases**, **`ui.componentsDir`** (folder prefix under `src/components`, default `ui`), and **Tailwind CSS path**. An annotated example ships with the library at **`node_modules/@lyda/kilo-ui/config/kilo.config.example.ts`**. If install used **`--ignore-scripts`**, run **`npx kilo-ui init`** once yourself.
20
+
21
+ You can still run **`npx kilo-ui init`** anytime to add missing scaffold files; use **`init --force`** to reset **`kilo.config.ts`** to defaults (back up first if you customized it).
21
22
 
22
23
  **Note:** The public npm name `kilo-ui` may point to a different project (not this Vue CLI). If `npm install -D kilo-ui` pulls SvelteKit or other wrong peers, use a scoped name or install from `file:` / Git. See [Installation](docs/docs/installation.md) in the docs site.
23
24
 
@@ -25,9 +26,10 @@ If the package was published with an installer hook, `npm install -D ...` will r
25
26
 
26
27
  `npx kilo-ui init` creates:
27
28
 
28
- - `components.json` - config and aliases
29
+ - **`kilo.config.ts`** - TypeScript project config (`defineConfig` from `@lyda/kilo-ui/config`), aliases, and **`ui.componentsDir`** (subfolder for installed components, default `ui`)
29
30
  - `src/styles/teamwork-ui.css` - Tailwind v4 import + Kilo UI theme tokens
30
31
  - `src/lib/utils.ts` - shared helper utilities
32
+ - `env.d.ts` (project root) - Vue + Vite TypeScript declarations for `*.vue` modules (skip if the file already exists); `init` also updates `tsconfig.app.json` / `tsconfig.json` with `paths` for `@/*` when possible
31
33
 
32
34
  Add the stylesheet once in your app entry, usually `src/main.ts`:
33
35
 
package/bin/kilo-ui.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createJiti } from 'jiti'
2
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
3
4
  import { dirname, join, relative, resolve } from 'node:path'
4
5
  import { fileURLToPath } from 'node:url'
@@ -6,15 +7,24 @@ import { fileURLToPath } from 'node:url'
6
7
  const __dirname = dirname(fileURLToPath(import.meta.url))
7
8
  const packageRoot = resolve(__dirname, '..')
8
9
  const registryRoot = join(packageRoot, 'registry')
10
+ const cliEntry = fileURLToPath(import.meta.url)
9
11
 
10
12
  const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'))
11
13
  const cliName = typeof pkg.name === 'string' ? pkg.name : 'kilo-ui'
14
+ const pkgName = cliName
12
15
 
13
16
  const commands = new Set(['init', 'add', 'list', 'help'])
14
17
  const [, , maybeCommand, ...args] = process.argv
15
18
  const command = commands.has(maybeCommand) ? maybeCommand : 'help'
16
19
 
17
- const configFile = 'components.json'
20
+ const KILO_TS = 'kilo.config.ts'
21
+ const KILO_MTS = 'kilo.config.mts'
22
+ const KILO_JS = 'kilo.config.js'
23
+ const KILO_MJS = 'kilo.config.mjs'
24
+ const CONFIG_MODULES = [KILO_TS, KILO_MTS, KILO_JS, KILO_MJS]
25
+
26
+ const UI_JSON = 'ui.json'
27
+ const LEGACY_JSON = 'components.json'
18
28
 
19
29
  function log(message = '') {
20
30
  console.log(message)
@@ -26,7 +36,8 @@ function fail(message) {
26
36
  }
27
37
 
28
38
  function readJson(path) {
29
- return JSON.parse(readFileSync(path, 'utf8'))
39
+ const raw = readFileSync(path, 'utf8').replace(/^\uFEFF/, '')
40
+ return JSON.parse(raw)
30
41
  }
31
42
 
32
43
  function writeJson(path, value) {
@@ -51,17 +62,82 @@ function registry() {
51
62
  return readJson(join(registryRoot, 'index.json'))
52
63
  }
53
64
 
65
+ const defaultAliases = {
66
+ components: '@/components',
67
+ composables: '@/composables',
68
+ lib: '@/lib',
69
+ types: '@/types',
70
+ }
71
+
72
+ function normalizeConfig(raw) {
73
+ const c = typeof raw === 'object' && raw !== null ? { ...raw } : {}
74
+ c.srcDir = c.srcDir || 'src'
75
+ c.aliases = { ...defaultAliases, ...(c.aliases || {}) }
76
+ c.tailwind = c.tailwind || { css: `${c.srcDir}/styles/teamwork-ui.css` }
77
+ const uiRaw =
78
+ typeof c.ui === 'object' && c.ui !== null ? { ...c.ui } : {}
79
+ const componentsPrefix = uiRaw.componentsPrefix
80
+ delete uiRaw.componentsPrefix
81
+ c.ui = { componentsDir: 'ui', ...uiRaw }
82
+ if (
83
+ (c.ui.componentsDir === undefined || c.ui.componentsDir === null) &&
84
+ componentsPrefix !== undefined &&
85
+ componentsPrefix !== null
86
+ ) {
87
+ c.ui.componentsDir = componentsPrefix
88
+ }
89
+ if (c.ui.componentsDir === undefined || c.ui.componentsDir === null) {
90
+ c.ui.componentsDir = 'ui'
91
+ }
92
+ return c
93
+ }
94
+
95
+ function hasKiloModuleConfig(cwd) {
96
+ return CONFIG_MODULES.some((name) => existsSync(join(cwd, name)))
97
+ }
98
+
99
+ function loadProjectConfigFromDisk(cwd) {
100
+ const jiti = createJiti(cliEntry, { interopDefault: true, cwd })
101
+
102
+ for (const name of CONFIG_MODULES) {
103
+ const p = join(cwd, name)
104
+ if (!existsSync(p)) continue
105
+ try {
106
+ const mod = jiti(p)
107
+ const raw = mod?.default ?? mod
108
+ if (!raw || typeof raw !== 'object') {
109
+ fail(`${name} must default-export a config object.`)
110
+ }
111
+ return normalizeConfig(raw)
112
+ } catch (e) {
113
+ const msg = e instanceof Error ? e.message : String(e)
114
+ fail(`Failed to load ${name}: ${msg}`)
115
+ }
116
+ }
117
+
118
+ const uiPath = join(cwd, UI_JSON)
119
+ if (existsSync(uiPath)) return normalizeConfig(readJson(uiPath))
120
+
121
+ const legacyPath = join(cwd, LEGACY_JSON)
122
+ if (existsSync(legacyPath)) return normalizeConfig(readJson(legacyPath))
123
+
124
+ return null
125
+ }
126
+
54
127
  function projectConfig() {
55
- const path = join(process.cwd(), configFile)
56
- if (!existsSync(path)) {
57
- fail(`Missing ${configFile}. Run "npx ${cliName} init" first.`)
128
+ const cwd = process.cwd()
129
+ const config = loadProjectConfigFromDisk(cwd)
130
+ if (!config) {
131
+ fail(
132
+ `Missing kilo.config.ts (or kilo.config.mjs, ${UI_JSON}, ${LEGACY_JSON}). Run "npx ${cliName} init" first.`,
133
+ )
58
134
  }
59
- return readJson(path)
135
+ return config
60
136
  }
61
137
 
62
138
  function aliasPath(config, key) {
63
139
  const value = config.aliases?.[key]
64
- if (!value) fail(`Missing aliases.${key} in ${configFile}`)
140
+ if (!value) fail(`Missing aliases.${key} in kilo.config / ${UI_JSON} / ${LEGACY_JSON}`)
65
141
  return value.replace(/^@\//, config.srcDir ? `${config.srcDir}/` : 'src/')
66
142
  }
67
143
 
@@ -69,8 +145,22 @@ function resolveTarget(config, alias, fileName) {
69
145
  return join(process.cwd(), aliasPath(config, alias), fileName)
70
146
  }
71
147
 
148
+ /** Path segments under the components alias (e.g. ["ui"] → …/components/ui/…). Empty string = no extra folder. */
149
+ function componentDirSegments(config) {
150
+ const dir = config.ui?.componentsDir
151
+ if (dir === '') return []
152
+ if (dir === undefined) return ['ui']
153
+ return String(dir).replace(/\\/g, '/').split('/').filter(Boolean)
154
+ }
155
+
72
156
  function componentFilePath(config, componentName, fileName) {
73
- return join(process.cwd(), aliasPath(config, 'components'), 'ui', componentName, fileName)
157
+ return join(
158
+ process.cwd(),
159
+ aliasPath(config, 'components'),
160
+ ...componentDirSegments(config),
161
+ componentName,
162
+ fileName,
163
+ )
74
164
  }
75
165
 
76
166
  function copyRegistryFile(sourceRelative, targetPath, config, force) {
@@ -84,13 +174,105 @@ function copyRegistryFile(sourceRelative, targetPath, config, force) {
84
174
  writeFile(targetPath, content, force)
85
175
  }
86
176
 
177
+ /**
178
+ * Vite resolves `@` from vite.config; TypeScript needs matching paths or `@/…` imports fail in the editor / vue-tsc.
179
+ * Vue templates usually keep env.d.ts at the project root next to tsconfig.app.json (not under src/).
180
+ */
181
+ function kiloConfigSerializable(config) {
182
+ const n = normalizeConfig(config)
183
+ return {
184
+ style: n.style ?? 'default',
185
+ srcDir: n.srcDir,
186
+ ui: n.ui,
187
+ tailwind: n.tailwind,
188
+ aliases: n.aliases,
189
+ }
190
+ }
191
+
192
+ function buildKiloConfigFileContent(config) {
193
+ const obj = kiloConfigSerializable(config)
194
+ const body = JSON.stringify(obj, null, 2)
195
+ const header = `// Kilo UI — app config (generated in your project root by ${pkgName}, not inside the library package)\n// ui.componentsDir = folder prefix under aliases.components (default "ui" → …/components/ui/<component>/)\n// Use "" for no prefix: …/components/<component>/\n\n`
196
+ return `${header}import { defineConfig } from '${pkgName}/config'\n\nexport default defineConfig(${body})\n`
197
+ }
198
+
199
+ /** Create kilo.config.ts when missing, or overwrite when force (unless a non-.ts kilo module is the only config). */
200
+ function writeKiloConfigTs(cwd, config, force) {
201
+ const kiloPath = join(cwd, KILO_TS)
202
+ if (hasKiloModuleConfig(cwd) && !force) return
203
+ if (!force && existsSync(kiloPath)) return
204
+
205
+ const rel = relative(cwd, kiloPath)
206
+ const existed = existsSync(kiloPath)
207
+ ensureDir(dirname(kiloPath))
208
+ writeFileSync(kiloPath, buildKiloConfigFileContent(config), 'utf8')
209
+ log(`${force || existed ? 'write' : 'add'} ${rel}`)
210
+ }
211
+
212
+ function mergeTsconfigForAliases(cwd, srcDir = 'src') {
213
+ const srcNorm = String(srcDir || 'src').replace(/\\/g, '/').replace(/\/$/, '') || 'src'
214
+ const pathPattern = `./${srcNorm}/*`
215
+ const appPath = join(cwd, 'tsconfig.app.json')
216
+ const rootPath = join(cwd, 'tsconfig.json')
217
+
218
+ function tryMerge(filePath) {
219
+ if (!existsSync(filePath)) return false
220
+ let parsed
221
+ try {
222
+ parsed = readJson(filePath)
223
+ } catch {
224
+ log(`skip ${relative(cwd, filePath)} (invalid JSON — fix or add paths manually)`)
225
+ return false
226
+ }
227
+ const relName = relative(cwd, filePath)
228
+ if (relName === 'tsconfig.json' && parsed.references?.length && !parsed.compilerOptions) {
229
+ return false
230
+ }
231
+ parsed.compilerOptions = parsed.compilerOptions || {}
232
+ const co = parsed.compilerOptions
233
+ const paths = { ...(co.paths || {}) }
234
+ const key = '@/*'
235
+ if (paths[key]?.[0] === pathPattern) {
236
+ log(`skip ${relName} (paths "@/*" already set)`)
237
+ return true
238
+ }
239
+ if (paths[key]?.length) {
240
+ log(
241
+ `skip ${relName} (paths "@/*" already defined — ensure it points at your ${srcNorm}/ folder for kilo-ui imports)`,
242
+ )
243
+ return true
244
+ }
245
+ paths[key] = [pathPattern]
246
+ co.paths = paths
247
+ if (co.baseUrl === undefined) co.baseUrl = '.'
248
+
249
+ if (Array.isArray(parsed.include)) {
250
+ const hasEnv = parsed.include.some((entry) => String(entry).includes('env.d.ts'))
251
+ if (!hasEnv) parsed.include.unshift('env.d.ts')
252
+ }
253
+
254
+ writeJson(filePath, parsed)
255
+ log(`update ${relName} (TypeScript paths for @ + env.d.ts in include)`)
256
+ return true
257
+ }
258
+
259
+ if (tryMerge(appPath)) return
260
+ if (tryMerge(rootPath)) return
261
+
262
+ log('\nTip: Add TypeScript paths so editors resolve @ imports (same folder as vite alias):')
263
+ log(` "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["${pathPattern}"] } }`)
264
+ }
265
+
87
266
  function init(args) {
88
267
  const force = args.includes('--force')
89
- const configPath = join(process.cwd(), configFile)
90
- const defaultConfig = {
91
- $schema: 'https://kilo-ui.local/schema.json',
268
+ const cwd = process.cwd()
269
+ const uiPath = join(cwd, UI_JSON)
270
+ const legacyPath = join(cwd, LEGACY_JSON)
271
+
272
+ const defaultConfig = normalizeConfig({
92
273
  style: 'default',
93
274
  srcDir: 'src',
275
+ ui: { componentsDir: 'ui' },
94
276
  tailwind: {
95
277
  css: 'src/styles/teamwork-ui.css',
96
278
  },
@@ -100,23 +282,43 @@ function init(args) {
100
282
  lib: '@/lib',
101
283
  types: '@/types',
102
284
  },
103
- }
104
- const config = existsSync(configPath) && !force ? readJson(configPath) : defaultConfig
285
+ })
286
+
287
+ let config
105
288
 
106
- if (existsSync(configPath) && !force) {
107
- log(`skip ${configFile} (already exists)`)
289
+ if (!force) {
290
+ if (hasKiloModuleConfig(cwd)) {
291
+ config = loadProjectConfigFromDisk(cwd)
292
+ if (!config) fail('Could not read kilo config.')
293
+ log(`skip kilo.config.* (already exists)`)
294
+ } else if (existsSync(uiPath)) {
295
+ config = normalizeConfig(readJson(uiPath))
296
+ log(`using ${UI_JSON} (legacy). Prefer ${KILO_TS} — run init on a fresh branch or add the file from docs.`)
297
+ } else if (existsSync(legacyPath)) {
298
+ config = normalizeConfig(readJson(legacyPath))
299
+ log(`using ${LEGACY_JSON} (legacy fields). Adding ${KILO_TS} if missing.`)
300
+ } else {
301
+ config = defaultConfig
302
+ }
108
303
  } else {
109
- writeJson(configPath, config)
110
- log(`write ${configFile}`)
304
+ config = defaultConfig
111
305
  }
112
306
 
307
+ writeKiloConfigTs(cwd, config, force)
308
+
113
309
  copyRegistryFile('styles/teamwork-ui.css', join(process.cwd(), config.tailwind.css), config, force)
114
310
  copyRegistryFile('lib/utils.ts', resolveTarget(config, 'lib', 'utils.ts'), config, force)
311
+ copyRegistryFile('vue/env.d.ts', join(process.cwd(), 'env.d.ts'), config, force)
312
+
313
+ mergeTsconfigForAliases(process.cwd(), config.srcDir || 'src')
115
314
 
315
+ const sub = componentDirSegments(config).join('/') || '(components root)'
116
316
  log('\nDone. Add this once in your app entry or main stylesheet:')
117
317
  log(` import './styles/${config.tailwind.css.replace(/^.*\//, '')}'`)
318
+ log(`\nComponents install under: ${aliasPath(config, 'components').replace(/\\/g, '/')}/${sub}/<name>/`)
118
319
  log('\nThen add components:')
119
320
  log(` npx ${cliName} add data-table app-navbar activity-log`)
321
+ log(`\nEdit ${KILO_TS} to change where components install (aliases, ui.componentsDir).`)
120
322
  }
121
323
 
122
324
  function add(args) {
@@ -163,6 +365,8 @@ Usage:
163
365
  npx ${cliName} add <component...> [--force]
164
366
  npx ${cliName} list
165
367
 
368
+ Config: ${KILO_TS} (or ${KILO_MJS}, legacy ${UI_JSON} / ${LEGACY_JSON}). Use defineConfig from "${pkgName}/config". Set ui.componentsDir for the folder under your components alias (default "ui").
369
+
166
370
  Examples:
167
371
  npx ${cliName} init
168
372
  npx ${cliName} add data-table app-navbar
@@ -1,20 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync } from 'node:fs'
3
- import { dirname, join, resolve } from 'node:path'
3
+ import { basename, dirname, join, resolve } from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { spawnSync } from 'node:child_process'
6
6
 
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url))
8
8
  const packageRoot = resolve(__dirname, '..')
9
9
 
10
- const initCwd = process.env.INIT_CWD
10
+ const initCwd = resolveConsumerProjectRoot()
11
11
  if (!initCwd) process.exit(0)
12
12
 
13
13
  // Avoid running when installing this repo itself.
14
- if (resolve(initCwd) === packageRoot) process.exit(0)
14
+ if (resolve(initCwd) === resolve(packageRoot)) process.exit(0)
15
15
 
16
- // Only run once; if the user already initialized, do nothing.
17
- if (existsSync(join(initCwd, 'components.json'))) process.exit(0)
16
+ // Only skip auto-init when a kilo config module already exists.
17
+ // Do not treat components.json / ui.json as "done" — shadcn and other tools use those too,
18
+ // and we still need to add kilo.config.ts.
19
+ const kiloModuleNames = ['kilo.config.ts', 'kilo.config.mts', 'kilo.config.js', 'kilo.config.mjs']
20
+ if (kiloModuleNames.some((name) => existsSync(join(initCwd, name)))) process.exit(0)
18
21
 
19
22
  const cliEntry = join(packageRoot, 'bin', 'kilo-ui.mjs')
20
23
 
@@ -24,3 +27,21 @@ const result = spawnSync(process.execPath, [cliEntry, 'init'], {
24
27
  })
25
28
 
26
29
  process.exit(result.status ?? 1)
30
+
31
+ /**
32
+ * Prefer npm's INIT_CWD or npm_config_local_prefix. If missing, infer the app root only when
33
+ * this package lives under node_modules (never guess from a bare repo path).
34
+ */
35
+ function resolveConsumerProjectRoot() {
36
+ const fromEnv = process.env.INIT_CWD || process.env.npm_config_local_prefix
37
+ if (fromEnv) return resolve(fromEnv)
38
+
39
+ const norm = packageRoot.replace(/\\/g, '/')
40
+ if (!norm.includes('/node_modules/')) return null
41
+
42
+ const parent = dirname(packageRoot)
43
+ if (basename(parent).startsWith('@')) {
44
+ return resolve(parent, '..', '..')
45
+ }
46
+ return resolve(parent, '..')
47
+ }
@@ -0,0 +1,26 @@
1
+ export interface KiloUiUserConfig {
2
+ /** @deprecated informational only */
3
+ $schema?: string
4
+ style?: string
5
+ srcDir?: string
6
+ ui?: {
7
+ /**
8
+ * Folder prefix under `aliases.components` where `kilo-ui add` places each component
9
+ * (default `"ui"` → e.g. `src/components/ui/<name>/`). Use `""` for `…/components/<name>/`.
10
+ */
11
+ componentsDir?: string
12
+ /** Same as `componentsDir` if you prefer the name “prefix”. */
13
+ componentsPrefix?: string
14
+ }
15
+ tailwind?: {
16
+ css: string
17
+ }
18
+ aliases: {
19
+ components: string
20
+ composables: string
21
+ lib: string
22
+ types: string
23
+ }
24
+ }
25
+
26
+ export function defineConfig(config: KiloUiUserConfig): KiloUiUserConfig
@@ -0,0 +1,4 @@
1
+ /** @param {import('./define-config.d.ts').KiloUiUserConfig} config */
2
+ export function defineConfig(config) {
3
+ return config
4
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Example config shipped with the npm package at:
3
+ * node_modules/@lyda/kilo-ui/config/kilo.config.example.ts
4
+ *
5
+ * Your Vue app does NOT edit this file. On `npm install`, the CLI creates
6
+ * **`kilo.config.ts` in your project root** (next to package.json).
7
+ *
8
+ * Change **`ui.componentsDir`** to set the folder prefix under your components path
9
+ * (e.g. `"ui"` → src/components/ui/data-table/…). Use `componentsPrefix` as an alias.
10
+ */
11
+ import { defineConfig } from '@lyda/kilo-ui/config'
12
+
13
+ export default defineConfig({
14
+ style: 'default',
15
+ srcDir: 'src',
16
+ ui: {
17
+ /** Folder prefix under `aliases.components` for copied registry components */
18
+ componentsDir: 'ui',
19
+ },
20
+ tailwind: {
21
+ css: 'src/styles/teamwork-ui.css',
22
+ },
23
+ aliases: {
24
+ components: '@/components',
25
+ composables: '@/composables',
26
+ lib: '@/lib',
27
+ types: '@/types',
28
+ },
29
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyda/kilo-ui",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "shadcn-style Vue component registry for team CRUD apps",
5
5
  "type": "module",
6
6
  "repository": {
@@ -17,8 +17,15 @@
17
17
  "bin": {
18
18
  "kilo-ui": "bin/kilo-ui.mjs"
19
19
  },
20
+ "exports": {
21
+ "./config": {
22
+ "types": "./config/define-config.d.ts",
23
+ "default": "./config/define-config.mjs"
24
+ }
25
+ },
20
26
  "files": [
21
27
  "bin",
28
+ "config",
22
29
  "registry",
23
30
  "README.md"
24
31
  ],
@@ -49,5 +56,8 @@
49
56
  "crud",
50
57
  "ui"
51
58
  ],
52
- "license": "MIT"
59
+ "license": "MIT",
60
+ "dependencies": {
61
+ "jiti": "^2.7.0"
62
+ }
53
63
  }
@@ -0,0 +1,7 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*.vue' {
4
+ import type { DefineComponent } from 'vue'
5
+ const component: DefineComponent<object, object, any>
6
+ export default component
7
+ }