uipkge-ng 0.1.0
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/LICENSE +22 -0
- package/README.md +371 -0
- package/dist/args.js +7 -0
- package/dist/commands/add.js +216 -0
- package/dist/commands/build.js +121 -0
- package/dist/commands/diff.js +140 -0
- package/dist/commands/info.js +122 -0
- package/dist/commands/init.js +154 -0
- package/dist/commands/list.js +103 -0
- package/dist/commands/mcp.js +149 -0
- package/dist/commands/view.js +48 -0
- package/dist/config.js +56 -0
- package/dist/env.js +33 -0
- package/dist/errors.js +9 -0
- package/dist/extras.js +111 -0
- package/dist/files.js +49 -0
- package/dist/index.js +162 -0
- package/dist/layout.js +114 -0
- package/dist/output.js +26 -0
- package/dist/packages.js +68 -0
- package/dist/project.js +145 -0
- package/dist/prompts.js +27 -0
- package/dist/registry.js +187 -0
- package/dist/resolve.js +60 -0
- package/dist/setup.js +169 -0
- package/package.json +50 -0
package/dist/setup.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Project setup that `init` performs: tsconfig paths, Tailwind v4, stylesheet import.
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { applyEdits, modify, parse, printParseErrorCode } from 'jsonc-parser';
|
|
6
|
+
import { UipkgeError } from './errors.js';
|
|
7
|
+
import { readTsChain } from './layout.js';
|
|
8
|
+
/** The two aliases every component source imports (`@/lib/utils`, `@/ui/button`). */
|
|
9
|
+
export const TS_PATHS = {
|
|
10
|
+
'@/*': './src/*',
|
|
11
|
+
'@/ui/*': './src/app/components/ui/*',
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Adds aliases to a tsconfig's `compilerOptions.paths`, keeping comments,
|
|
15
|
+
* formatting and any existing mappings. `seed` is written first when the file
|
|
16
|
+
* has no `paths` of its own (to keep inherited ones). Returns the new text, or
|
|
17
|
+
* null when nothing needed to change.
|
|
18
|
+
*/
|
|
19
|
+
export function addTsconfigPaths(text, wanted = TS_PATHS, seed = {}) {
|
|
20
|
+
const errors = [];
|
|
21
|
+
const doc = parse(text, errors, { allowTrailingComma: true, disallowComments: false });
|
|
22
|
+
if (errors.length) {
|
|
23
|
+
throw new UipkgeError(`Could not parse tsconfig: ${printParseErrorCode(errors[0].error)} at offset ${errors[0].offset}`);
|
|
24
|
+
}
|
|
25
|
+
let out = text;
|
|
26
|
+
let changed = false;
|
|
27
|
+
const existing = doc?.compilerOptions?.paths ?? { ...seed };
|
|
28
|
+
const set = (alias, value) => {
|
|
29
|
+
out = applyEdits(out, modify(out, ['compilerOptions', 'paths', alias], value, { formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' } }));
|
|
30
|
+
changed = true;
|
|
31
|
+
};
|
|
32
|
+
if (!doc?.compilerOptions?.paths)
|
|
33
|
+
for (const [alias, value] of Object.entries(seed))
|
|
34
|
+
set(alias, value);
|
|
35
|
+
for (const [alias, target] of Object.entries(wanted)) {
|
|
36
|
+
const current = existing[alias];
|
|
37
|
+
const list = Array.isArray(current) ? current.filter((v) => typeof v === 'string') : [];
|
|
38
|
+
if (list.includes(target))
|
|
39
|
+
continue;
|
|
40
|
+
set(alias, [...list, target]);
|
|
41
|
+
}
|
|
42
|
+
return changed ? out : null;
|
|
43
|
+
}
|
|
44
|
+
/** The tsconfigs init adds paths to: tsconfig.json, or a workspace app's tsconfig.app.json and tsconfig.spec.json. */
|
|
45
|
+
export function tsconfigsToEdit(root) {
|
|
46
|
+
if (existsSync(path.join(root, 'tsconfig.json')))
|
|
47
|
+
return ['tsconfig.json'];
|
|
48
|
+
return ['tsconfig.app.json', 'tsconfig.spec.json'].filter(f => existsSync(path.join(root, f)));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `addTsconfigPaths` for `file` in the app at `root`: targets are written
|
|
52
|
+
* relative to where TypeScript will resolve them (an inherited baseUrl, else
|
|
53
|
+
* the file's folder), and inherited paths are kept, since declaring `paths`
|
|
54
|
+
* replaces them.
|
|
55
|
+
*/
|
|
56
|
+
export function appTsconfigEdit(root, file, text) {
|
|
57
|
+
const abs = path.join(root, file);
|
|
58
|
+
const chain = readTsChain(abs);
|
|
59
|
+
const base = chain.baseUrl ?? path.dirname(abs);
|
|
60
|
+
const rel = (target) => {
|
|
61
|
+
const r = posixPath(path.relative(base, path.resolve(root, target)));
|
|
62
|
+
return r.startsWith('.') ? r : `./${r}`;
|
|
63
|
+
};
|
|
64
|
+
const wanted = Object.fromEntries(Object.entries(TS_PATHS).map(([alias, target]) => [alias, rel(target)]));
|
|
65
|
+
const seed = {};
|
|
66
|
+
if (!chain.ownPaths && chain.paths) {
|
|
67
|
+
const from = chain.baseUrl ?? chain.pathsDir ?? root;
|
|
68
|
+
for (const [alias, targets] of Object.entries(chain.paths))
|
|
69
|
+
seed[alias] = targets.map(t => rel(path.relative(root, path.resolve(from, t))));
|
|
70
|
+
}
|
|
71
|
+
return addTsconfigPaths(text, wanted, seed);
|
|
72
|
+
}
|
|
73
|
+
const posixPath = (p) => p.replace(/\\/g, '/');
|
|
74
|
+
// ─── Tailwind ────────────────────────────────────────────────────────────────
|
|
75
|
+
export const POSTCSS_FILES = [
|
|
76
|
+
'.postcssrc.json',
|
|
77
|
+
'.postcssrc',
|
|
78
|
+
'postcss.config.js',
|
|
79
|
+
'postcss.config.mjs',
|
|
80
|
+
'postcss.config.cjs',
|
|
81
|
+
'postcss.config.ts',
|
|
82
|
+
];
|
|
83
|
+
export const TAILWIND_POSTCSS = '@tailwindcss/postcss';
|
|
84
|
+
/**
|
|
85
|
+
* What init must do so the tokens' `@import 'tailwindcss'` resolves. Angular CLI
|
|
86
|
+
* apps run Tailwind through PostCSS; Analog builds with Vite, which ignores
|
|
87
|
+
* PostCSS files and needs the Vite plugin in vite.config.ts (user code, not
|
|
88
|
+
* edited here).
|
|
89
|
+
*/
|
|
90
|
+
export function planTailwind(input) {
|
|
91
|
+
if (input.hasTailwind)
|
|
92
|
+
return { kind: 'skip' };
|
|
93
|
+
if (input.isAnalog)
|
|
94
|
+
return { kind: 'vite', devPackages: ['tailwindcss', '@tailwindcss/vite'] };
|
|
95
|
+
const devPackages = ['tailwindcss', TAILWIND_POSTCSS, 'postcss'];
|
|
96
|
+
const found = input.postcss;
|
|
97
|
+
if (!found)
|
|
98
|
+
return { kind: 'postcss', devPackages, config: { file: '.postcssrc.json', action: 'create' } };
|
|
99
|
+
if (found.content.includes(TAILWIND_POSTCSS))
|
|
100
|
+
return { kind: 'postcss', devPackages, config: { file: found.file, action: 'keep' } };
|
|
101
|
+
const json = found.file === '.postcssrc.json' || found.file === '.postcssrc';
|
|
102
|
+
return { kind: 'postcss', devPackages, config: { file: found.file, action: json ? 'merge' : 'manual' } };
|
|
103
|
+
}
|
|
104
|
+
/** Puts the Tailwind plugin first in a JSON PostCSS config, keeping the others. */
|
|
105
|
+
export function withTailwindPlugin(content) {
|
|
106
|
+
const doc = (content.trim() ? parse(content) : {});
|
|
107
|
+
const plugins = doc.plugins && typeof doc.plugins === 'object' ? doc.plugins : {};
|
|
108
|
+
return JSON.stringify({ ...doc, plugins: { [TAILWIND_POSTCSS]: {}, ...plugins } }, null, 2) + '\n';
|
|
109
|
+
}
|
|
110
|
+
export async function readPostcssConfig(root) {
|
|
111
|
+
for (const file of POSTCSS_FILES) {
|
|
112
|
+
const abs = path.join(root, file);
|
|
113
|
+
if (existsSync(abs))
|
|
114
|
+
return { file, content: await readFile(abs, 'utf8') };
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
// ─── Stylesheet ──────────────────────────────────────────────────────────────
|
|
119
|
+
/**
|
|
120
|
+
* Makes `stylesheet` import the tokens file (path relative to it). The line goes
|
|
121
|
+
* first: CSS requires @import before other rules. Returns the new text, or null
|
|
122
|
+
* when the import is already there.
|
|
123
|
+
*/
|
|
124
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^$(){}|[\]\\]/g, '\\$&');
|
|
125
|
+
export function withTokensImport(stylesheet, importPath) {
|
|
126
|
+
const line = `@import '${importPath}';`;
|
|
127
|
+
const already = new RegExp(`@import\\s+(url\\()?['"]${escapeRegExp(importPath)}['"]`);
|
|
128
|
+
if (already.test(stylesheet))
|
|
129
|
+
return null;
|
|
130
|
+
return stylesheet ? `${line}\n${stylesheet}` : `${line}\n`;
|
|
131
|
+
}
|
|
132
|
+
/** `src/styles.css` + `src/uipkge.css` → `./uipkge.css`. */
|
|
133
|
+
export function relativeImport(fromFile, toFile) {
|
|
134
|
+
const rel = path.posix.relative(path.posix.dirname(fromFile.replace(/\\/g, '/')), toFile.replace(/\\/g, '/'));
|
|
135
|
+
return rel.startsWith('.') ? rel : `./${rel}`;
|
|
136
|
+
}
|
|
137
|
+
export async function writeIfChanged(file, next) {
|
|
138
|
+
if (next === null)
|
|
139
|
+
return false;
|
|
140
|
+
await writeFile(file, next, 'utf8');
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Puts `entry` (relative to the workspace root) first in the app's global
|
|
145
|
+
* `styles` for every target that has them (build, test), keeping angular.json's
|
|
146
|
+
* formatting. Returns whether anything changed.
|
|
147
|
+
*/
|
|
148
|
+
export async function addGlobalStyle(packageRoot, project, entry) {
|
|
149
|
+
const file = path.join(packageRoot, 'angular.json');
|
|
150
|
+
let text = await readFile(file, 'utf8');
|
|
151
|
+
const doc = parse(text);
|
|
152
|
+
const entryOf = doc.projects?.[project];
|
|
153
|
+
const key = entryOf?.architect ? 'architect' : 'targets';
|
|
154
|
+
let changed = false;
|
|
155
|
+
for (const [name, target] of Object.entries(entryOf?.[key] ?? {})) {
|
|
156
|
+
const styles = target.options?.styles;
|
|
157
|
+
if (!Array.isArray(styles) || styles.some(s => (typeof s === 'string' ? s : s.input) === entry))
|
|
158
|
+
continue;
|
|
159
|
+
const edits = modify(text, ['projects', project, key, name, 'options', 'styles', 0], entry, {
|
|
160
|
+
isArrayInsertion: true,
|
|
161
|
+
formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' },
|
|
162
|
+
});
|
|
163
|
+
text = applyEdits(text, edits);
|
|
164
|
+
changed = true;
|
|
165
|
+
}
|
|
166
|
+
if (changed)
|
|
167
|
+
await writeFile(file, text, 'utf8');
|
|
168
|
+
return changed;
|
|
169
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "uipkge-ng",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Add uipkge Angular components to your project: copy the source, own the code.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Uday Adaka (https://uipkge.dev)",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"homepage": "https://uipkge.dev",
|
|
9
|
+
"bugs": "https://github.com/uday-a/uipkge-cli/issues",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/uday-a/uipkge-cli.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"uipkge",
|
|
16
|
+
"angular",
|
|
17
|
+
"components",
|
|
18
|
+
"tailwindcss",
|
|
19
|
+
"cli",
|
|
20
|
+
"registry",
|
|
21
|
+
"shadcn",
|
|
22
|
+
"shadcn-angular"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"uipkge-ng": "dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20.12"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.build.json",
|
|
35
|
+
"dev": "tsx src/index.ts",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@clack/prompts": "^1.8.1",
|
|
41
|
+
"diff": "^9.0.0",
|
|
42
|
+
"jsonc-parser": "^3.3.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
|
+
"tsx": "^4.20.0",
|
|
47
|
+
"typescript": "^5.9.3",
|
|
48
|
+
"vitest": "^3.2.7"
|
|
49
|
+
}
|
|
50
|
+
}
|