create-ec-app 1.8.0 → 1.9.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/README.md +72 -17
- package/dist/cssScope.js +3 -5
- package/dist/cssScope.js.map +1 -1
- package/dist/index.d.ts +46 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +129 -53
- package/dist/index.js.map +1 -1
- package/dist/libFunctions.d.ts +13 -6
- package/dist/libFunctions.d.ts.map +1 -1
- package/dist/libFunctions.js +24 -9
- package/dist/libFunctions.js.map +1 -1
- package/dist/pcf.d.ts.map +1 -1
- package/dist/pcf.js +4 -1
- package/dist/pcf.js.map +1 -1
- package/dist/portalContainers.js +7 -5
- package/dist/portalContainers.js.map +1 -1
- package/package.json +18 -11
- package/scripts/build-generated.mjs +59 -0
- package/scripts/refresh-shadcn-template.ts +406 -0
- package/scripts/smoke-scaffold.mjs +245 -0
- package/templates/base/eslint.config.js +1 -1
- package/templates/base/package-lock.json +380 -476
- package/templates/base/package.json +14 -19
- package/templates/pcf/base/package-lock.json +35 -53
- package/templates/targets/code-apps/AGENTS.md +1 -1
- package/templates/targets/code-apps/CLAUDE.md +1 -0
- package/templates/targets/code-apps/package.patch.json +1 -1
- package/templates/targets/power-pages/AGENTS.md +1 -1
- package/templates/targets/power-pages/CLAUDE.md +1 -0
- package/templates/targets/power-pages/README.md +22 -2
- package/templates/targets/power-pages/src/App.patch.tsx +3 -1
- package/templates/targets/power-pages/src/components/shared/AuthError.tsx +18 -0
- package/templates/targets/power-pages/src/context/AuthContext.tsx +0 -4
- package/templates/targets/swa/CLAUDE.md +1 -0
- package/templates/targets/webresource/AGENTS.md +5 -4
- package/templates/targets/webresource/CLAUDE.md +1 -0
- package/templates/targets/webresource/README.md +5 -5
- package/templates/ui/kendo/package.patch.json +2 -2
- package/templates/ui/shadcn-ui/SHADCN_TEMPLATE.md +20 -0
- package/templates/ui/shadcn-ui/package.patch.json +18 -9
- package/templates/ui/shadcn-ui/src/components/ui/accordion.tsx +79 -0
- package/templates/ui/shadcn-ui/src/components/ui/alert-dialog.tsx +199 -0
- package/templates/ui/shadcn-ui/src/components/ui/alert.tsx +76 -0
- package/templates/ui/shadcn-ui/src/components/ui/aspect-ratio.tsx +11 -0
- package/templates/ui/shadcn-ui/src/components/ui/attachment.tsx +206 -0
- package/templates/ui/shadcn-ui/src/components/ui/avatar.tsx +110 -0
- package/templates/ui/shadcn-ui/src/components/ui/badge.tsx +49 -0
- package/templates/ui/shadcn-ui/src/components/ui/breadcrumb.tsx +122 -0
- package/templates/ui/shadcn-ui/src/components/ui/bubble.tsx +125 -0
- package/templates/ui/shadcn-ui/src/components/ui/button-group.tsx +83 -0
- package/templates/ui/shadcn-ui/src/components/ui/button.tsx +67 -0
- package/templates/ui/shadcn-ui/src/components/ui/calendar.tsx +222 -0
- package/templates/ui/shadcn-ui/src/components/ui/card.tsx +103 -0
- package/templates/ui/shadcn-ui/src/components/ui/carousel.tsx +240 -0
- package/templates/ui/shadcn-ui/src/components/ui/chart.tsx +373 -0
- package/templates/ui/shadcn-ui/src/components/ui/checkbox.tsx +31 -0
- package/templates/ui/shadcn-ui/src/components/ui/collapsible.tsx +33 -0
- package/templates/ui/shadcn-ui/src/components/ui/combobox.tsx +299 -0
- package/templates/ui/shadcn-ui/src/components/ui/command.tsx +195 -0
- package/templates/ui/shadcn-ui/src/components/ui/context-menu.tsx +264 -0
- package/templates/ui/shadcn-ui/src/components/ui/dialog.tsx +170 -0
- package/templates/ui/shadcn-ui/src/components/ui/direction.tsx +22 -0
- package/templates/ui/shadcn-ui/src/components/ui/drawer.tsx +134 -0
- package/templates/ui/shadcn-ui/src/components/ui/dropdown-menu.tsx +272 -0
- package/templates/ui/shadcn-ui/src/components/ui/empty.tsx +104 -0
- package/templates/ui/shadcn-ui/src/components/ui/field.tsx +236 -0
- package/templates/ui/shadcn-ui/src/components/ui/hover-card.tsx +44 -0
- package/templates/ui/shadcn-ui/src/components/ui/input-group.tsx +156 -0
- package/templates/ui/shadcn-ui/src/components/ui/input-otp.tsx +87 -0
- package/templates/ui/shadcn-ui/src/components/ui/input.tsx +19 -0
- package/templates/ui/shadcn-ui/src/components/ui/item.tsx +196 -0
- package/templates/ui/shadcn-ui/src/components/ui/kbd.tsx +26 -0
- package/templates/ui/shadcn-ui/src/components/ui/label.tsx +22 -0
- package/templates/ui/shadcn-ui/src/components/ui/marker.tsx +69 -0
- package/templates/ui/shadcn-ui/src/components/ui/menubar.tsx +282 -0
- package/templates/ui/shadcn-ui/src/components/ui/message-scroller.tsx +129 -0
- package/templates/ui/shadcn-ui/src/components/ui/message.tsx +92 -0
- package/templates/ui/shadcn-ui/src/components/ui/native-select.tsx +61 -0
- package/templates/ui/shadcn-ui/src/components/ui/navigation-menu.tsx +164 -0
- package/templates/ui/shadcn-ui/src/components/ui/pagination.tsx +129 -0
- package/templates/ui/shadcn-ui/src/components/ui/popover.tsx +89 -0
- package/templates/ui/shadcn-ui/src/components/ui/progress.tsx +31 -0
- package/templates/ui/shadcn-ui/src/components/ui/radio-group.tsx +42 -0
- package/templates/ui/shadcn-ui/src/components/ui/resizable.tsx +50 -0
- package/templates/ui/shadcn-ui/src/components/ui/scroll-area.tsx +53 -0
- package/templates/ui/shadcn-ui/src/components/ui/select.tsx +194 -0
- package/templates/ui/shadcn-ui/src/components/ui/separator.tsx +26 -0
- package/templates/ui/shadcn-ui/src/components/ui/sheet.tsx +149 -0
- package/templates/ui/shadcn-ui/src/components/ui/sidebar.tsx +702 -0
- package/templates/ui/shadcn-ui/src/components/ui/skeleton.tsx +13 -0
- package/templates/ui/shadcn-ui/src/components/ui/slider.tsx +59 -0
- package/templates/ui/shadcn-ui/src/components/ui/sonner.tsx +47 -0
- package/templates/ui/shadcn-ui/src/components/ui/spinner.tsx +10 -0
- package/templates/ui/shadcn-ui/src/components/ui/switch.tsx +33 -0
- package/templates/ui/shadcn-ui/src/components/ui/table.tsx +114 -0
- package/templates/ui/shadcn-ui/src/components/ui/tabs.tsx +90 -0
- package/templates/ui/shadcn-ui/src/components/ui/textarea.tsx +18 -0
- package/templates/ui/shadcn-ui/src/components/ui/toggle-group.tsx +87 -0
- package/templates/ui/shadcn-ui/src/components/ui/toggle.tsx +45 -0
- package/templates/ui/shadcn-ui/src/components/ui/tooltip.tsx +59 -0
- package/templates/ui/shadcn-ui/src/index.patch.css +0 -118
- package/templates/ui/shadcn-ui/src/runtime/PortalContainer.ts +8 -0
- package/templates/base/biome.json +0 -54
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path, { dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import fs from "fs-extra";
|
|
6
|
+
import { localizeShadcnPortals } from "../src/portalContainers.ts";
|
|
7
|
+
|
|
8
|
+
const SHADCN_CLI_VERSION = "4.12.0";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const REPO_ROOT = path.resolve(__dirname, "..");
|
|
13
|
+
const TEMPLATE_DIR = path.join(REPO_ROOT, "templates", "ui", "shadcn-ui");
|
|
14
|
+
const BASE_TEMPLATE_DIR = path.join(REPO_ROOT, "templates", "base");
|
|
15
|
+
const KEEP_TEMP = process.argv.includes("--keep-temp");
|
|
16
|
+
const SHADCN_UTILS_TEMPLATE = `import { clsx, type ClassValue } from "clsx";
|
|
17
|
+
import { twMerge } from "tailwind-merge";
|
|
18
|
+
|
|
19
|
+
export function cn(...inputs: ClassValue[]) {
|
|
20
|
+
\treturn twMerge(clsx(inputs));
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
const FALLBACK_DEPENDENCY_VERSIONS: Record<string, string> = {
|
|
24
|
+
"class-variance-authority": "0.7.1",
|
|
25
|
+
clsx: "2.1.1",
|
|
26
|
+
"lucide-react": "1.21.0",
|
|
27
|
+
"radix-ui": "1.6.0",
|
|
28
|
+
shadcn: SHADCN_CLI_VERSION,
|
|
29
|
+
"tailwind-merge": "3.6.0",
|
|
30
|
+
};
|
|
31
|
+
const FALLBACK_DEV_DEPENDENCY_VERSIONS: Record<string, string> = {
|
|
32
|
+
"tw-animate-css": "1.4.0",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type DependencySection = "dependencies" | "devDependencies";
|
|
36
|
+
|
|
37
|
+
interface PackageJson {
|
|
38
|
+
dependencies?: Record<string, string>;
|
|
39
|
+
devDependencies?: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface PackageLock {
|
|
43
|
+
packages?: Record<string, { version?: string }>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function main() {
|
|
47
|
+
const tempRoot = await fs.mkdtemp(
|
|
48
|
+
path.join(os.tmpdir(), "create-ec-app-shadcn-"),
|
|
49
|
+
);
|
|
50
|
+
const tempProjectDir = path.join(tempRoot, "app");
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await fs.copy(BASE_TEMPLATE_DIR, tempProjectDir, {
|
|
54
|
+
filter: (source) => path.basename(source) !== "node_modules",
|
|
55
|
+
});
|
|
56
|
+
await fs.copy(
|
|
57
|
+
path.join(TEMPLATE_DIR, "components.json"),
|
|
58
|
+
path.join(tempProjectDir, "components.json"),
|
|
59
|
+
);
|
|
60
|
+
await fs.outputFile(
|
|
61
|
+
path.join(tempProjectDir, "src", "lib", "utils.ts"),
|
|
62
|
+
SHADCN_UTILS_TEMPLATE,
|
|
63
|
+
"utf8",
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
67
|
+
execFileSync(
|
|
68
|
+
npxBin,
|
|
69
|
+
[
|
|
70
|
+
`shadcn@${SHADCN_CLI_VERSION}`,
|
|
71
|
+
"add",
|
|
72
|
+
"--all",
|
|
73
|
+
"--yes",
|
|
74
|
+
"--overwrite",
|
|
75
|
+
],
|
|
76
|
+
{ cwd: tempProjectDir, stdio: "inherit" },
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
await ensureShadcnTailwindImport(tempProjectDir);
|
|
80
|
+
await localizeShadcnPortals(tempProjectDir);
|
|
81
|
+
await copyGeneratedTemplateFiles(tempProjectDir);
|
|
82
|
+
await updatePackagePatch(tempProjectDir);
|
|
83
|
+
await writeMetadata();
|
|
84
|
+
|
|
85
|
+
if (KEEP_TEMP) {
|
|
86
|
+
console.log(`Kept temporary shadcn app at ${tempProjectDir}`);
|
|
87
|
+
}
|
|
88
|
+
} finally {
|
|
89
|
+
if (!KEEP_TEMP) {
|
|
90
|
+
await fs.remove(tempRoot);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function ensureShadcnTailwindImport(projectDir: string): Promise<void> {
|
|
96
|
+
const cssPath = path.join(projectDir, "src", "index.css");
|
|
97
|
+
const source = await fs.readFile(cssPath, "utf8");
|
|
98
|
+
|
|
99
|
+
if (source.includes('shadcn/tailwind.css')) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const importLine = '@import "shadcn/tailwind.css";';
|
|
104
|
+
const updated = source.includes('@import "tailwindcss";')
|
|
105
|
+
? source.replace(
|
|
106
|
+
'@import "tailwindcss";',
|
|
107
|
+
`@import "tailwindcss";\n${importLine}`,
|
|
108
|
+
)
|
|
109
|
+
: `${importLine}\n${source}`;
|
|
110
|
+
|
|
111
|
+
await fs.writeFile(cssPath, updated, "utf8");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function copyGeneratedTemplateFiles(tempProjectDir: string): Promise<void> {
|
|
115
|
+
const templateSrcDir = path.join(TEMPLATE_DIR, "src");
|
|
116
|
+
const generatedSrcDir = path.join(tempProjectDir, "src");
|
|
117
|
+
|
|
118
|
+
for (const relPath of ["components", "hooks", "lib", "runtime"]) {
|
|
119
|
+
await fs.remove(path.join(templateSrcDir, relPath));
|
|
120
|
+
const generatedPath = path.join(generatedSrcDir, relPath);
|
|
121
|
+
if (await fs.pathExists(generatedPath)) {
|
|
122
|
+
await fs.copy(generatedPath, path.join(templateSrcDir, relPath));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await fs.copy(
|
|
127
|
+
path.join(generatedSrcDir, "index.css"),
|
|
128
|
+
path.join(templateSrcDir, "index.patch.css"),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function updatePackagePatch(tempProjectDir: string): Promise<void> {
|
|
133
|
+
const basePackage = await readPackageJson(
|
|
134
|
+
path.join(BASE_TEMPLATE_DIR, "package.json"),
|
|
135
|
+
);
|
|
136
|
+
const generatedPackage = await readPackageJson(
|
|
137
|
+
path.join(tempProjectDir, "package.json"),
|
|
138
|
+
);
|
|
139
|
+
const packageLock = await readPackageLock(
|
|
140
|
+
path.join(tempProjectDir, "package-lock.json"),
|
|
141
|
+
);
|
|
142
|
+
const patch: PackageJson = {};
|
|
143
|
+
|
|
144
|
+
for (const section of [
|
|
145
|
+
"dependencies",
|
|
146
|
+
"devDependencies",
|
|
147
|
+
] satisfies DependencySection[]) {
|
|
148
|
+
const generatedDependencies = generatedPackage[section] ?? {};
|
|
149
|
+
const baseDependencies = basePackage[section] ?? {};
|
|
150
|
+
const diff: Record<string, string> = {};
|
|
151
|
+
|
|
152
|
+
for (const [name, range] of Object.entries(generatedDependencies).sort()) {
|
|
153
|
+
if (baseDependencies[name] === range) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
diff[name] = getLockedVersion(packageLock, name) ?? stripRange(range);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (Object.keys(diff).length > 0) {
|
|
161
|
+
patch[section] = diff;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const sourceImports = await collectTemplateSourceImports(
|
|
166
|
+
path.join(tempProjectDir, "src"),
|
|
167
|
+
);
|
|
168
|
+
for (const packageName of sourceImports) {
|
|
169
|
+
if (basePackage.dependencies?.[packageName]) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (basePackage.devDependencies?.[packageName]) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (patch.dependencies?.[packageName]) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (patch.devDependencies?.[packageName]) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const generatedSection = findDependencySection(
|
|
186
|
+
generatedPackage,
|
|
187
|
+
packageName,
|
|
188
|
+
);
|
|
189
|
+
const fallbackSection =
|
|
190
|
+
packageName in FALLBACK_DEV_DEPENDENCY_VERSIONS
|
|
191
|
+
? "devDependencies"
|
|
192
|
+
: "dependencies";
|
|
193
|
+
const section = generatedSection ?? fallbackSection;
|
|
194
|
+
const fallbackVersions =
|
|
195
|
+
section === "devDependencies"
|
|
196
|
+
? FALLBACK_DEV_DEPENDENCY_VERSIONS
|
|
197
|
+
: FALLBACK_DEPENDENCY_VERSIONS;
|
|
198
|
+
const version =
|
|
199
|
+
getLockedVersion(packageLock, packageName) ??
|
|
200
|
+
(generatedSection
|
|
201
|
+
? stripRange(generatedPackage[generatedSection]?.[packageName] ?? "")
|
|
202
|
+
: fallbackVersions[packageName]);
|
|
203
|
+
|
|
204
|
+
if (!version) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Could not determine a version for shadcn source dependency "${packageName}".`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
patch[section] = {
|
|
211
|
+
...(patch[section] ?? {}),
|
|
212
|
+
[packageName]: version,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const section of [
|
|
217
|
+
"dependencies",
|
|
218
|
+
"devDependencies",
|
|
219
|
+
] satisfies DependencySection[]) {
|
|
220
|
+
if (patch[section]) {
|
|
221
|
+
patch[section] = Object.fromEntries(
|
|
222
|
+
Object.entries(patch[section]).sort(([a], [b]) => a.localeCompare(b)),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await fs.writeFile(
|
|
228
|
+
path.join(TEMPLATE_DIR, "package.patch.json"),
|
|
229
|
+
`${JSON.stringify(patch, null, "\t")}\n`,
|
|
230
|
+
"utf8",
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function collectTemplateSourceImports(srcDir: string): Promise<Set<string>> {
|
|
235
|
+
const imports = new Set<string>();
|
|
236
|
+
await collectImportsFromDir(srcDir, imports);
|
|
237
|
+
return imports;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function collectImportsFromDir(
|
|
241
|
+
dirPath: string,
|
|
242
|
+
imports: Set<string>,
|
|
243
|
+
): Promise<void> {
|
|
244
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
245
|
+
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
248
|
+
if (entry.isDirectory()) {
|
|
249
|
+
await collectImportsFromDir(fullPath, imports);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!/\.(css|ts|tsx)$/.test(entry.name)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const source = await fs.readFile(fullPath, "utf8");
|
|
258
|
+
for (const specifier of readImportSpecifiers(source)) {
|
|
259
|
+
const packageName = toPackageName(specifier);
|
|
260
|
+
if (packageName) {
|
|
261
|
+
imports.add(packageName);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function readImportSpecifiers(source: string): string[] {
|
|
268
|
+
const specifiers: string[] = [];
|
|
269
|
+
const importPattern =
|
|
270
|
+
/from\s+["']([^"']+)["']|import\s+["']([^"']+)["']|@import\s+["']([^"']+)["']/g;
|
|
271
|
+
|
|
272
|
+
for (const match of source.matchAll(importPattern)) {
|
|
273
|
+
const specifier = match[1] ?? match[2] ?? match[3];
|
|
274
|
+
if (specifier) {
|
|
275
|
+
specifiers.push(specifier);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return specifiers;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function toPackageName(specifier: string): string | undefined {
|
|
283
|
+
if (
|
|
284
|
+
specifier.startsWith(".") ||
|
|
285
|
+
specifier.startsWith("@/") ||
|
|
286
|
+
specifier.startsWith("#")
|
|
287
|
+
) {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const parts = specifier.split("/");
|
|
292
|
+
if (specifier.startsWith("@")) {
|
|
293
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return parts[0] ?? specifier;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function findDependencySection(
|
|
300
|
+
packageJson: PackageJson,
|
|
301
|
+
packageName: string,
|
|
302
|
+
): DependencySection | undefined {
|
|
303
|
+
if (packageJson.dependencies?.[packageName]) {
|
|
304
|
+
return "dependencies";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (packageJson.devDependencies?.[packageName]) {
|
|
308
|
+
return "devDependencies";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function writeMetadata(): Promise<void> {
|
|
315
|
+
const content = `# shadcn Template Snapshot
|
|
316
|
+
|
|
317
|
+
This template contains committed shadcn component source.
|
|
318
|
+
|
|
319
|
+
Generated with:
|
|
320
|
+
|
|
321
|
+
- shadcn CLI: ${SHADCN_CLI_VERSION}
|
|
322
|
+
- style: radix-nova
|
|
323
|
+
- base color: neutral
|
|
324
|
+
- Tailwind: v4
|
|
325
|
+
- generated by: \`npm run refresh:shadcn-template\`
|
|
326
|
+
|
|
327
|
+
Normal \`create-ec-app\` scaffolding does not run \`npx shadcn\`.
|
|
328
|
+
To refresh this snapshot, run:
|
|
329
|
+
|
|
330
|
+
\`\`\`bash
|
|
331
|
+
npm run refresh:shadcn-template
|
|
332
|
+
\`\`\`
|
|
333
|
+
|
|
334
|
+
After refreshing, generate a test app and run a build before committing.
|
|
335
|
+
`;
|
|
336
|
+
|
|
337
|
+
await fs.writeFile(path.join(TEMPLATE_DIR, "SHADCN_TEMPLATE.md"), content, "utf8");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function readPackageJson(filePath: string): Promise<PackageJson> {
|
|
341
|
+
const json = (await fs.readJson(filePath)) as unknown;
|
|
342
|
+
if (!isPackageJson(json)) {
|
|
343
|
+
throw new Error(`Expected package JSON object in ${filePath}.`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return json;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function readPackageLock(filePath: string): Promise<PackageLock> {
|
|
350
|
+
if (!(await fs.pathExists(filePath))) {
|
|
351
|
+
return {};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const json = (await fs.readJson(filePath)) as unknown;
|
|
355
|
+
if (!isObject(json)) {
|
|
356
|
+
return {};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return json as PackageLock;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function isPackageJson(value: unknown): value is PackageJson {
|
|
363
|
+
if (!isObject(value)) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
isOptionalDependencyRecord(value.dependencies) &&
|
|
369
|
+
isOptionalDependencyRecord(value.devDependencies)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function isOptionalDependencyRecord(
|
|
374
|
+
value: unknown,
|
|
375
|
+
): value is Record<string, string> | undefined {
|
|
376
|
+
if (value === undefined) {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!isObject(value)) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return Object.values(value).every((entry) => typeof entry === "string");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function getLockedVersion(
|
|
388
|
+
packageLock: PackageLock,
|
|
389
|
+
packageName: string,
|
|
390
|
+
): string | undefined {
|
|
391
|
+
const entry = packageLock.packages?.[`node_modules/${packageName}`];
|
|
392
|
+
return typeof entry?.version === "string" ? entry.version : undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function stripRange(range: string): string {
|
|
396
|
+
return range.replace(/^[~^]/, "");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
400
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
main().catch((error) => {
|
|
404
|
+
console.error(error);
|
|
405
|
+
process.exit(1);
|
|
406
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const cliPath = path.join(repoRoot, "dist", "index.js");
|
|
11
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "create-ec-app-smoke-"));
|
|
12
|
+
|
|
13
|
+
const matrix = [
|
|
14
|
+
["webresource", "kendo"],
|
|
15
|
+
["webresource", "shadcn-ui"],
|
|
16
|
+
["power-pages", "kendo"],
|
|
17
|
+
["power-pages", "shadcn-ui"],
|
|
18
|
+
["swa", "kendo"],
|
|
19
|
+
["swa", "shadcn-ui"],
|
|
20
|
+
["code-apps", "kendo"],
|
|
21
|
+
["code-apps", "shadcn-ui"],
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
execFileSync("npm", ["run", "build"], { cwd: repoRoot, stdio: "inherit" });
|
|
26
|
+
|
|
27
|
+
const helpOutput = execFileSync("node", [cliPath, "--help"], {
|
|
28
|
+
cwd: tempRoot,
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
stdio: "pipe",
|
|
31
|
+
});
|
|
32
|
+
assert(
|
|
33
|
+
helpOutput.includes("create-ec-app --project-name my-app"),
|
|
34
|
+
"Help output includes scaffold examples",
|
|
35
|
+
);
|
|
36
|
+
assert(helpOutput.includes("--skip-git"), "Help output documents --skip-git");
|
|
37
|
+
assert(helpOutput.includes("--force"), "Help output documents --force");
|
|
38
|
+
|
|
39
|
+
for (const [target, ui] of matrix) {
|
|
40
|
+
const projectName = `${target}-${ui}`;
|
|
41
|
+
const projectDir = path.join(tempRoot, projectName);
|
|
42
|
+
|
|
43
|
+
execFileSync(
|
|
44
|
+
"node",
|
|
45
|
+
[
|
|
46
|
+
cliPath,
|
|
47
|
+
"--project-name",
|
|
48
|
+
projectName,
|
|
49
|
+
"--target",
|
|
50
|
+
target,
|
|
51
|
+
"--ui",
|
|
52
|
+
ui,
|
|
53
|
+
"--no-install",
|
|
54
|
+
"--skip-git",
|
|
55
|
+
],
|
|
56
|
+
{ cwd: tempRoot, stdio: "pipe" },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
assertPath(projectDir, `${projectName} project folder`);
|
|
60
|
+
assertPath(path.join(projectDir, "package.json"), `${projectName} package.json`);
|
|
61
|
+
assertPath(path.join(projectDir, "src", "App.tsx"), `${projectName} App.tsx`);
|
|
62
|
+
assertMissing(path.join(projectDir, ".git"), `${projectName} .git directory`);
|
|
63
|
+
assertRegularFileContains(
|
|
64
|
+
path.join(projectDir, "CLAUDE.md"),
|
|
65
|
+
"@AGENTS.md",
|
|
66
|
+
`${projectName} Claude guidance pointer`,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (ui === "shadcn-ui") {
|
|
70
|
+
assertPath(
|
|
71
|
+
path.join(projectDir, "components.json"),
|
|
72
|
+
`${projectName} components.json`,
|
|
73
|
+
);
|
|
74
|
+
assertPath(
|
|
75
|
+
path.join(projectDir, "src", "components", "ui"),
|
|
76
|
+
`${projectName} shadcn components directory`,
|
|
77
|
+
);
|
|
78
|
+
assertPath(
|
|
79
|
+
path.join(projectDir, "src", "components", "ui", "button.tsx"),
|
|
80
|
+
`${projectName} shadcn button`,
|
|
81
|
+
);
|
|
82
|
+
assertPath(
|
|
83
|
+
path.join(projectDir, "src", "lib", "utils.ts"),
|
|
84
|
+
`${projectName} shadcn utils`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (ui === "kendo") {
|
|
89
|
+
const packageJson = readJson(path.join(projectDir, "package.json"));
|
|
90
|
+
assert(
|
|
91
|
+
packageJson.dependencies?.["@progress/kendo-react-buttons"],
|
|
92
|
+
`${projectName} has Kendo dependencies`,
|
|
93
|
+
);
|
|
94
|
+
assertFileContains(
|
|
95
|
+
path.join(projectDir, "src", "main.tsx"),
|
|
96
|
+
"@progress/kendo-theme-fluent/dist/all.css",
|
|
97
|
+
`${projectName} imports Kendo theme CSS`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (target === "webresource") {
|
|
102
|
+
assertPath(
|
|
103
|
+
path.join(projectDir, "src", "services", "AuthService.ts"),
|
|
104
|
+
`${projectName} webresource auth service`,
|
|
105
|
+
);
|
|
106
|
+
assertPath(path.join(projectDir, "token.json"), `${projectName} token.json`);
|
|
107
|
+
assertFileContains(
|
|
108
|
+
path.join(projectDir, "vite.config.ts"),
|
|
109
|
+
'base: "./"',
|
|
110
|
+
`${projectName} webresource base config`,
|
|
111
|
+
);
|
|
112
|
+
assertFileContains(
|
|
113
|
+
path.join(projectDir, "vite.config.ts"),
|
|
114
|
+
"cssCodeSplit: false",
|
|
115
|
+
`${projectName} webresource cssCodeSplit config`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (target === "power-pages") {
|
|
120
|
+
assertPath(
|
|
121
|
+
path.join(projectDir, "src", "context", "AuthContext.tsx"),
|
|
122
|
+
`${projectName} power pages auth context`,
|
|
123
|
+
);
|
|
124
|
+
assertPath(
|
|
125
|
+
path.join(projectDir, "src", "components", "shared", "AuthError.tsx"),
|
|
126
|
+
`${projectName} power pages auth error component`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (target === "swa") {
|
|
131
|
+
assertPath(
|
|
132
|
+
path.join(projectDir, "staticwebapp.config.json"),
|
|
133
|
+
`${projectName} staticwebapp.config.json`,
|
|
134
|
+
);
|
|
135
|
+
assertPath(
|
|
136
|
+
path.join(projectDir, "swa-cli.config.json"),
|
|
137
|
+
`${projectName} swa-cli.config.json`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (target === "code-apps") {
|
|
142
|
+
assertPath(
|
|
143
|
+
path.join(projectDir, "power.config.example.json"),
|
|
144
|
+
`${projectName} power.config.example.json`,
|
|
145
|
+
);
|
|
146
|
+
assertFileContains(
|
|
147
|
+
path.join(projectDir, "vite.config.ts"),
|
|
148
|
+
"powerApps()",
|
|
149
|
+
`${projectName} vite powerApps plugin`,
|
|
150
|
+
);
|
|
151
|
+
assertMissing(path.join(projectDir, "token.json"), `${projectName} token.json`);
|
|
152
|
+
assertMissing(
|
|
153
|
+
path.join(projectDir, "src", "services", "AuthService.ts"),
|
|
154
|
+
`${projectName} webresource auth service`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const guardedProject = path.join(tempRoot, "existing-project");
|
|
160
|
+
fs.mkdirSync(guardedProject);
|
|
161
|
+
fs.writeFileSync(path.join(guardedProject, "keep.txt"), "do not overwrite");
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
execFileSync(
|
|
165
|
+
"node",
|
|
166
|
+
[
|
|
167
|
+
cliPath,
|
|
168
|
+
"--project-name",
|
|
169
|
+
"existing-project",
|
|
170
|
+
"--target",
|
|
171
|
+
"webresource",
|
|
172
|
+
"--ui",
|
|
173
|
+
"kendo",
|
|
174
|
+
"--no-install",
|
|
175
|
+
"--skip-git",
|
|
176
|
+
],
|
|
177
|
+
{ cwd: tempRoot, encoding: "utf8", stdio: "pipe" },
|
|
178
|
+
);
|
|
179
|
+
throw new Error("Expected existing non-empty project directory to fail");
|
|
180
|
+
} catch (error) {
|
|
181
|
+
const output = `${error.stdout ?? ""}${error.stderr ?? ""}`;
|
|
182
|
+
assert(
|
|
183
|
+
output.includes("already exists and is not empty"),
|
|
184
|
+
"Existing non-empty directory fails with a clear error",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
execFileSync(
|
|
189
|
+
"node",
|
|
190
|
+
[
|
|
191
|
+
cliPath,
|
|
192
|
+
"--project-name",
|
|
193
|
+
"existing-project",
|
|
194
|
+
"--target",
|
|
195
|
+
"webresource",
|
|
196
|
+
"--ui",
|
|
197
|
+
"kendo",
|
|
198
|
+
"--no-install",
|
|
199
|
+
"--skip-git",
|
|
200
|
+
"--force",
|
|
201
|
+
],
|
|
202
|
+
{ cwd: tempRoot, stdio: "pipe" },
|
|
203
|
+
);
|
|
204
|
+
assertMissing(
|
|
205
|
+
path.join(guardedProject, "keep.txt"),
|
|
206
|
+
"forced scaffold marker file",
|
|
207
|
+
);
|
|
208
|
+
assertPath(
|
|
209
|
+
path.join(guardedProject, "package.json"),
|
|
210
|
+
"forced scaffold package.json",
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
console.log("Scaffold smoke checks passed.");
|
|
214
|
+
} finally {
|
|
215
|
+
fs.rmSync(tempRoot, { force: true, recursive: true });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function readJson(filePath) {
|
|
219
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function assertPath(filePath, label) {
|
|
223
|
+
assert(fs.existsSync(filePath), `Missing ${label}: ${filePath}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function assertMissing(filePath, label) {
|
|
227
|
+
assert(!fs.existsSync(filePath), `Expected ${label} to be absent: ${filePath}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function assertFileContains(filePath, expected, label) {
|
|
231
|
+
const source = fs.readFileSync(filePath, "utf8");
|
|
232
|
+
assert(source.includes(expected), `${label} did not contain ${expected}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function assertRegularFileContains(filePath, expected, label) {
|
|
236
|
+
const stat = fs.lstatSync(filePath);
|
|
237
|
+
assert(stat.isFile(), `${label} should be a regular file: ${filePath}`);
|
|
238
|
+
assertFileContains(filePath, expected, label);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function assert(condition, message) {
|
|
242
|
+
if (!condition) {
|
|
243
|
+
throw new Error(message);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -6,7 +6,7 @@ import tseslint from "typescript-eslint";
|
|
|
6
6
|
import { defineConfig, globalIgnores } from "eslint/config";
|
|
7
7
|
|
|
8
8
|
export default defineConfig([
|
|
9
|
-
globalIgnores(["dist"]),
|
|
9
|
+
globalIgnores(["dist", "src/components/ui/**", "src/hooks/use-mobile.ts"]),
|
|
10
10
|
{
|
|
11
11
|
files: ["**/*.{ts,tsx}"],
|
|
12
12
|
extends: [
|