@configjs/cli 1.1.2 → 1.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 +25 -5
- package/dist/{check-7PYMMMZK.js → check-XDGAGYOE.js} +2 -2
- package/dist/{chunk-OJGTPK6N.js → chunk-4VHPGJVU.js} +1674 -11
- package/dist/{chunk-QDVUNUTK.js → chunk-BVXGN3AC.js} +36 -2
- package/dist/{install-UTFQ545S.js → chunk-OAAGGK2H.js} +139 -137
- package/dist/{chunk-VJ254HJY.js → chunk-WKYUK64P.js} +110 -16
- package/dist/cli.js +17 -6
- package/dist/install-APYIRHSN.js +258 -0
- package/dist/install-nextjs-C3LEKJLY.js +353 -0
- package/dist/{installed-G2RXXXZ6.js → installed-IKSARZIK.js} +1 -1
- package/dist/{list-NW6ENYK6.js → list-IJK225B3.js} +1 -1
- package/dist/{remove-QDF5BA6U.js → remove-IIT34Y3T.js} +1 -1
- package/package.json +1 -1
|
@@ -1533,7 +1533,7 @@ var jotaiPlugin = {
|
|
|
1533
1533
|
description: "State management atomique",
|
|
1534
1534
|
category: "state" /* STATE */,
|
|
1535
1535
|
version: "^2.16.1",
|
|
1536
|
-
frameworks: ["react"],
|
|
1536
|
+
frameworks: ["react", "nextjs"],
|
|
1537
1537
|
incompatibleWith: ["@reduxjs/toolkit", "zustand"],
|
|
1538
1538
|
/**
|
|
1539
1539
|
* Détecte si Jotai est déjà installé
|
|
@@ -2344,7 +2344,7 @@ var axiosPlugin = {
|
|
|
2344
2344
|
description: "Client HTTP bas\xE9 sur les promesses",
|
|
2345
2345
|
category: "http" /* HTTP */,
|
|
2346
2346
|
version: "^1.13.2",
|
|
2347
|
-
frameworks: ["react", "vue", "svelte"],
|
|
2347
|
+
frameworks: ["react", "vue", "svelte", "nextjs"],
|
|
2348
2348
|
/**
|
|
2349
2349
|
* Détecte si Axios est déjà installé
|
|
2350
2350
|
*/
|
|
@@ -2679,7 +2679,7 @@ var tanstackQueryPlugin = {
|
|
|
2679
2679
|
description: "Data fetching et caching",
|
|
2680
2680
|
category: "http" /* HTTP */,
|
|
2681
2681
|
version: "^5.90.16",
|
|
2682
|
-
frameworks: ["react"],
|
|
2682
|
+
frameworks: ["react", "nextjs"],
|
|
2683
2683
|
/**
|
|
2684
2684
|
* Détecte si TanStack Query est déjà installé
|
|
2685
2685
|
*/
|
|
@@ -4421,7 +4421,7 @@ var reactHookFormPlugin = {
|
|
|
4421
4421
|
description: "Gestion de formulaires performante pour React",
|
|
4422
4422
|
category: "forms" /* FORMS */,
|
|
4423
4423
|
version: "^7.69.0",
|
|
4424
|
-
frameworks: ["react"],
|
|
4424
|
+
frameworks: ["react", "nextjs"],
|
|
4425
4425
|
/**
|
|
4426
4426
|
* Détecte si React Hook Form est déjà installé
|
|
4427
4427
|
*/
|
|
@@ -4946,7 +4946,7 @@ var zodPlugin = {
|
|
|
4946
4946
|
description: "Validation de sch\xE9mas TypeScript-first",
|
|
4947
4947
|
category: "forms" /* FORMS */,
|
|
4948
4948
|
version: "^3.24.1",
|
|
4949
|
-
frameworks: ["react", "vue", "svelte"],
|
|
4949
|
+
frameworks: ["react", "vue", "svelte", "nextjs"],
|
|
4950
4950
|
/**
|
|
4951
4951
|
* Détecte si Zod est déjà installé
|
|
4952
4952
|
*/
|
|
@@ -5599,7 +5599,7 @@ var radixUiPlugin = {
|
|
|
5599
5599
|
description: "Composants UI headless et accessibles",
|
|
5600
5600
|
category: "ui" /* UI */,
|
|
5601
5601
|
version: "^1.2.4",
|
|
5602
|
-
frameworks: ["react"],
|
|
5602
|
+
frameworks: ["react", "nextjs"],
|
|
5603
5603
|
/**
|
|
5604
5604
|
* Détecte si Radix UI est déjà installé
|
|
5605
5605
|
*/
|
|
@@ -6329,7 +6329,7 @@ var reactIconsPlugin = {
|
|
|
6329
6329
|
description: "Biblioth\xE8que d'ic\xF4nes pour React",
|
|
6330
6330
|
category: "ui" /* UI */,
|
|
6331
6331
|
version: "^5.3.0",
|
|
6332
|
-
frameworks: ["react"],
|
|
6332
|
+
frameworks: ["react", "nextjs"],
|
|
6333
6333
|
/**
|
|
6334
6334
|
* Détecte si React Icons est déjà installé
|
|
6335
6335
|
*/
|
|
@@ -6954,7 +6954,7 @@ var eslintPlugin = {
|
|
|
6954
6954
|
description: "Linter JavaScript/TypeScript",
|
|
6955
6955
|
category: "tooling" /* TOOLING */,
|
|
6956
6956
|
version: "^9.39.2",
|
|
6957
|
-
frameworks: ["react", "vue", "svelte"],
|
|
6957
|
+
frameworks: ["react", "vue", "svelte", "nextjs"],
|
|
6958
6958
|
/**
|
|
6959
6959
|
* Détecte si ESLint est déjà installé
|
|
6960
6960
|
*/
|
|
@@ -7204,7 +7204,7 @@ var prettierPlugin = {
|
|
|
7204
7204
|
description: "Formateur de code",
|
|
7205
7205
|
category: "tooling" /* TOOLING */,
|
|
7206
7206
|
version: "^3.7.4",
|
|
7207
|
-
frameworks: ["react", "vue", "svelte"],
|
|
7207
|
+
frameworks: ["react", "vue", "svelte", "nextjs"],
|
|
7208
7208
|
/**
|
|
7209
7209
|
* Détecte si Prettier est déjà installé
|
|
7210
7210
|
*/
|
|
@@ -7582,7 +7582,7 @@ var dateFnsPlugin = {
|
|
|
7582
7582
|
description: "Manipulation de dates moderne",
|
|
7583
7583
|
category: "tooling" /* TOOLING */,
|
|
7584
7584
|
version: "^4.1.0",
|
|
7585
|
-
frameworks: ["react", "vue", "svelte"],
|
|
7585
|
+
frameworks: ["react", "vue", "svelte", "nextjs"],
|
|
7586
7586
|
/**
|
|
7587
7587
|
* Détecte si date-fns est déjà installé
|
|
7588
7588
|
*/
|
|
@@ -8083,6 +8083,1661 @@ describe('Example Component', () => {
|
|
|
8083
8083
|
`;
|
|
8084
8084
|
}
|
|
8085
8085
|
|
|
8086
|
+
// src/plugins/css/tailwindcss-nextjs.ts
|
|
8087
|
+
import { join as join24 } from "path";
|
|
8088
|
+
var tailwindcssNextjsPlugin = {
|
|
8089
|
+
name: "tailwindcss-nextjs",
|
|
8090
|
+
displayName: "TailwindCSS (Next.js)",
|
|
8091
|
+
description: "Framework CSS utilitaire pour Next.js",
|
|
8092
|
+
category: "css" /* CSS */,
|
|
8093
|
+
version: "^3.4.1",
|
|
8094
|
+
frameworks: ["nextjs"],
|
|
8095
|
+
/**
|
|
8096
|
+
* Détecte si TailwindCSS est déjà installé
|
|
8097
|
+
*/
|
|
8098
|
+
detect: (ctx) => {
|
|
8099
|
+
return ctx.dependencies["tailwindcss"] !== void 0 || ctx.devDependencies["tailwindcss"] !== void 0 || ctx.dependencies["postcss"] !== void 0 || ctx.devDependencies["postcss"] !== void 0 || ctx.dependencies["autoprefixer"] !== void 0 || ctx.devDependencies["autoprefixer"] !== void 0;
|
|
8100
|
+
},
|
|
8101
|
+
/**
|
|
8102
|
+
* Installe TailwindCSS, PostCSS et Autoprefixer pour Next.js
|
|
8103
|
+
*/
|
|
8104
|
+
async install(ctx) {
|
|
8105
|
+
if (this.detect?.(ctx)) {
|
|
8106
|
+
logger.info("TailwindCSS is already installed");
|
|
8107
|
+
return {
|
|
8108
|
+
packages: {},
|
|
8109
|
+
success: true,
|
|
8110
|
+
message: "TailwindCSS already installed"
|
|
8111
|
+
};
|
|
8112
|
+
}
|
|
8113
|
+
const packages = ["tailwindcss", "postcss", "autoprefixer"];
|
|
8114
|
+
try {
|
|
8115
|
+
await installPackages(packages, {
|
|
8116
|
+
dev: true,
|
|
8117
|
+
packageManager: ctx.packageManager,
|
|
8118
|
+
projectRoot: ctx.projectRoot,
|
|
8119
|
+
exact: false,
|
|
8120
|
+
silent: false
|
|
8121
|
+
});
|
|
8122
|
+
logger.info("Successfully installed TailwindCSS for Next.js");
|
|
8123
|
+
return {
|
|
8124
|
+
packages: {
|
|
8125
|
+
devDependencies: packages
|
|
8126
|
+
},
|
|
8127
|
+
success: true,
|
|
8128
|
+
message: `Installed ${packages.join(", ")}`
|
|
8129
|
+
};
|
|
8130
|
+
} catch (error) {
|
|
8131
|
+
logger.error("Failed to install TailwindCSS:", error);
|
|
8132
|
+
return {
|
|
8133
|
+
packages: {},
|
|
8134
|
+
success: false,
|
|
8135
|
+
message: `Failed to install TailwindCSS: ${error instanceof Error ? error.message : String(error)}`
|
|
8136
|
+
};
|
|
8137
|
+
}
|
|
8138
|
+
},
|
|
8139
|
+
/**
|
|
8140
|
+
* Configure TailwindCSS dans le projet Next.js
|
|
8141
|
+
*
|
|
8142
|
+
* Crée/modifie :
|
|
8143
|
+
* - tailwind.config.js (ou .ts) : Configuration TailwindCSS
|
|
8144
|
+
* - postcss.config.js (ou .mjs) : Configuration PostCSS
|
|
8145
|
+
* - app/globals.css ou styles/globals.css : Import TailwindCSS
|
|
8146
|
+
*/
|
|
8147
|
+
async configure(ctx) {
|
|
8148
|
+
const backupManager = new BackupManager();
|
|
8149
|
+
const writer = new ConfigWriter(backupManager);
|
|
8150
|
+
const files = [];
|
|
8151
|
+
const projectRoot = ctx.projectRoot;
|
|
8152
|
+
const extension = ctx.typescript ? "ts" : "js";
|
|
8153
|
+
const postcssExtension = ctx.typescript ? "js" : "mjs";
|
|
8154
|
+
try {
|
|
8155
|
+
const tailwindConfigPath = join24(
|
|
8156
|
+
projectRoot,
|
|
8157
|
+
`tailwind.config.${extension}`
|
|
8158
|
+
);
|
|
8159
|
+
const tailwindConfigExists = await checkPathExists(tailwindConfigPath);
|
|
8160
|
+
if (tailwindConfigExists) {
|
|
8161
|
+
const existingContent = await readFileContent(tailwindConfigPath);
|
|
8162
|
+
if (existingContent.includes("content:") && existingContent.includes("theme:")) {
|
|
8163
|
+
logger.info("TailwindCSS config already exists");
|
|
8164
|
+
} else {
|
|
8165
|
+
const tailwindConfig = getTailwindConfigContent(ctx, extension);
|
|
8166
|
+
await writer.writeFile(tailwindConfigPath, tailwindConfig, {
|
|
8167
|
+
backup: true
|
|
8168
|
+
});
|
|
8169
|
+
files.push({
|
|
8170
|
+
type: "modify",
|
|
8171
|
+
path: normalizePath(tailwindConfigPath),
|
|
8172
|
+
content: tailwindConfig,
|
|
8173
|
+
backup: true
|
|
8174
|
+
});
|
|
8175
|
+
}
|
|
8176
|
+
} else {
|
|
8177
|
+
const tailwindConfig = getTailwindConfigContent(ctx, extension);
|
|
8178
|
+
await writer.createFile(tailwindConfigPath, tailwindConfig);
|
|
8179
|
+
files.push({
|
|
8180
|
+
type: "create",
|
|
8181
|
+
path: normalizePath(tailwindConfigPath),
|
|
8182
|
+
content: tailwindConfig,
|
|
8183
|
+
backup: false
|
|
8184
|
+
});
|
|
8185
|
+
logger.info(`Created tailwind.config.${extension}`);
|
|
8186
|
+
}
|
|
8187
|
+
const postcssConfigPath = join24(
|
|
8188
|
+
projectRoot,
|
|
8189
|
+
`postcss.config.${postcssExtension}`
|
|
8190
|
+
);
|
|
8191
|
+
const postcssConfigExists = await checkPathExists(postcssConfigPath);
|
|
8192
|
+
if (!postcssConfigExists) {
|
|
8193
|
+
const postcssConfig = getPostcssConfigContent(postcssExtension);
|
|
8194
|
+
await writer.createFile(postcssConfigPath, postcssConfig);
|
|
8195
|
+
files.push({
|
|
8196
|
+
type: "create",
|
|
8197
|
+
path: normalizePath(postcssConfigPath),
|
|
8198
|
+
content: postcssConfig,
|
|
8199
|
+
backup: false
|
|
8200
|
+
});
|
|
8201
|
+
logger.info(`Created postcss.config.${postcssExtension}`);
|
|
8202
|
+
}
|
|
8203
|
+
const cssFiles = [
|
|
8204
|
+
join24(projectRoot, "app", "globals.css"),
|
|
8205
|
+
join24(projectRoot, ctx.srcDir, "app", "globals.css"),
|
|
8206
|
+
join24(projectRoot, "styles", "globals.css"),
|
|
8207
|
+
join24(projectRoot, ctx.srcDir, "styles", "globals.css")
|
|
8208
|
+
];
|
|
8209
|
+
let cssFileModified = false;
|
|
8210
|
+
for (const cssPath of cssFiles) {
|
|
8211
|
+
const cssExists = await checkPathExists(cssPath);
|
|
8212
|
+
if (cssExists) {
|
|
8213
|
+
const cssContent = await readFileContent(cssPath);
|
|
8214
|
+
const modifiedCss = injectTailwindDirectives(cssContent);
|
|
8215
|
+
await writer.writeFile(cssPath, modifiedCss, { backup: true });
|
|
8216
|
+
files.push({
|
|
8217
|
+
type: "modify",
|
|
8218
|
+
path: normalizePath(cssPath),
|
|
8219
|
+
content: modifiedCss,
|
|
8220
|
+
backup: true
|
|
8221
|
+
});
|
|
8222
|
+
logger.info(`Updated ${cssPath} with TailwindCSS directives`);
|
|
8223
|
+
cssFileModified = true;
|
|
8224
|
+
break;
|
|
8225
|
+
}
|
|
8226
|
+
}
|
|
8227
|
+
if (!cssFileModified) {
|
|
8228
|
+
const cssPath = join24(projectRoot, "app", "globals.css");
|
|
8229
|
+
const cssContent = getCssContent2();
|
|
8230
|
+
await writer.createFile(cssPath, cssContent);
|
|
8231
|
+
files.push({
|
|
8232
|
+
type: "create",
|
|
8233
|
+
path: normalizePath(cssPath),
|
|
8234
|
+
content: cssContent,
|
|
8235
|
+
backup: false
|
|
8236
|
+
});
|
|
8237
|
+
logger.info(`Created ${cssPath} with TailwindCSS directives`);
|
|
8238
|
+
}
|
|
8239
|
+
return {
|
|
8240
|
+
files,
|
|
8241
|
+
success: true,
|
|
8242
|
+
message: "TailwindCSS configured successfully for Next.js"
|
|
8243
|
+
};
|
|
8244
|
+
} catch (error) {
|
|
8245
|
+
logger.error("Failed to configure TailwindCSS:", error);
|
|
8246
|
+
return {
|
|
8247
|
+
files,
|
|
8248
|
+
success: false,
|
|
8249
|
+
message: `Failed to configure TailwindCSS: ${error instanceof Error ? error.message : String(error)}`
|
|
8250
|
+
};
|
|
8251
|
+
}
|
|
8252
|
+
},
|
|
8253
|
+
/**
|
|
8254
|
+
* Rollback de la configuration TailwindCSS
|
|
8255
|
+
*/
|
|
8256
|
+
async rollback(_ctx) {
|
|
8257
|
+
const backupManager = new BackupManager();
|
|
8258
|
+
try {
|
|
8259
|
+
await backupManager.restoreAll();
|
|
8260
|
+
logger.info("TailwindCSS configuration rolled back");
|
|
8261
|
+
} catch (error) {
|
|
8262
|
+
logger.error("Failed to rollback TailwindCSS configuration:", error);
|
|
8263
|
+
throw error;
|
|
8264
|
+
}
|
|
8265
|
+
}
|
|
8266
|
+
};
|
|
8267
|
+
function getTailwindConfigContent(ctx, extension) {
|
|
8268
|
+
const isTS = extension === "ts";
|
|
8269
|
+
const contentPaths = [
|
|
8270
|
+
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
|
8271
|
+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
8272
|
+
"./app/**/*.{js,ts,jsx,tsx,mdx}"
|
|
8273
|
+
];
|
|
8274
|
+
if (ctx.srcDir && ctx.srcDir !== "src") {
|
|
8275
|
+
contentPaths.unshift(`./${ctx.srcDir}/**/*.{js,ts,jsx,tsx,mdx}`);
|
|
8276
|
+
}
|
|
8277
|
+
const contentArray = contentPaths.map((path) => ` "${path}"`).join(",\n");
|
|
8278
|
+
if (isTS) {
|
|
8279
|
+
return `import type { Config } from 'tailwindcss'
|
|
8280
|
+
|
|
8281
|
+
const config: Config = {
|
|
8282
|
+
content: [
|
|
8283
|
+
${contentArray},
|
|
8284
|
+
],
|
|
8285
|
+
theme: {
|
|
8286
|
+
extend: {},
|
|
8287
|
+
},
|
|
8288
|
+
plugins: [],
|
|
8289
|
+
}
|
|
8290
|
+
export default config
|
|
8291
|
+
`;
|
|
8292
|
+
}
|
|
8293
|
+
return `/** @type {import('tailwindcss').Config} */
|
|
8294
|
+
module.exports = {
|
|
8295
|
+
content: [
|
|
8296
|
+
${contentArray},
|
|
8297
|
+
],
|
|
8298
|
+
theme: {
|
|
8299
|
+
extend: {},
|
|
8300
|
+
},
|
|
8301
|
+
plugins: [],
|
|
8302
|
+
}
|
|
8303
|
+
`;
|
|
8304
|
+
}
|
|
8305
|
+
function getPostcssConfigContent(extension) {
|
|
8306
|
+
if (extension === "mjs") {
|
|
8307
|
+
return `/** @type {import('postcss-load-config').Config} */
|
|
8308
|
+
const config = {
|
|
8309
|
+
plugins: {
|
|
8310
|
+
tailwindcss: {},
|
|
8311
|
+
autoprefixer: {},
|
|
8312
|
+
},
|
|
8313
|
+
}
|
|
8314
|
+
|
|
8315
|
+
export default config
|
|
8316
|
+
`;
|
|
8317
|
+
}
|
|
8318
|
+
return `module.exports = {
|
|
8319
|
+
plugins: {
|
|
8320
|
+
tailwindcss: {},
|
|
8321
|
+
autoprefixer: {},
|
|
8322
|
+
},
|
|
8323
|
+
}
|
|
8324
|
+
`;
|
|
8325
|
+
}
|
|
8326
|
+
function getCssContent2() {
|
|
8327
|
+
return `@tailwind base;
|
|
8328
|
+
@tailwind components;
|
|
8329
|
+
@tailwind utilities;
|
|
8330
|
+
`;
|
|
8331
|
+
}
|
|
8332
|
+
function injectTailwindDirectives(content) {
|
|
8333
|
+
if (content.includes("@tailwind base") || content.includes("@tailwind components") || content.includes("@tailwind utilities")) {
|
|
8334
|
+
logger.warn("TailwindCSS directives already present in CSS file");
|
|
8335
|
+
return content;
|
|
8336
|
+
}
|
|
8337
|
+
return `@tailwind base;
|
|
8338
|
+
@tailwind components;
|
|
8339
|
+
@tailwind utilities;
|
|
8340
|
+
|
|
8341
|
+
${content}`;
|
|
8342
|
+
}
|
|
8343
|
+
|
|
8344
|
+
// src/plugins/ui/shadcn-ui-nextjs.ts
|
|
8345
|
+
import { join as join25 } from "path";
|
|
8346
|
+
var shadcnUiNextjsPlugin = {
|
|
8347
|
+
name: "shadcn-ui-nextjs",
|
|
8348
|
+
displayName: "Shadcn/ui (Next.js)",
|
|
8349
|
+
description: "Composants UI accessibles et personnalisables pour Next.js",
|
|
8350
|
+
category: "ui" /* UI */,
|
|
8351
|
+
version: "^3.6.2",
|
|
8352
|
+
frameworks: ["nextjs"],
|
|
8353
|
+
requires: ["tailwindcss"],
|
|
8354
|
+
/**
|
|
8355
|
+
* Détecte si Shadcn/ui est déjà configuré
|
|
8356
|
+
*/
|
|
8357
|
+
detect: (ctx) => {
|
|
8358
|
+
return ctx.dependencies["class-variance-authority"] !== void 0 || ctx.devDependencies["class-variance-authority"] !== void 0 || ctx.dependencies["@radix-ui/react-slot"] !== void 0 || ctx.devDependencies["@radix-ui/react-slot"] !== void 0;
|
|
8359
|
+
},
|
|
8360
|
+
/**
|
|
8361
|
+
* Installe les dépendances nécessaires pour Shadcn/ui
|
|
8362
|
+
*/
|
|
8363
|
+
async install(ctx) {
|
|
8364
|
+
if (this.detect?.(ctx)) {
|
|
8365
|
+
logger.info("Shadcn/ui dependencies are already installed");
|
|
8366
|
+
return {
|
|
8367
|
+
packages: {},
|
|
8368
|
+
success: true,
|
|
8369
|
+
message: "Shadcn/ui dependencies already installed"
|
|
8370
|
+
};
|
|
8371
|
+
}
|
|
8372
|
+
try {
|
|
8373
|
+
const packages = [
|
|
8374
|
+
"class-variance-authority",
|
|
8375
|
+
"clsx",
|
|
8376
|
+
"tailwind-merge",
|
|
8377
|
+
"@radix-ui/react-slot"
|
|
8378
|
+
];
|
|
8379
|
+
const radixPrimitives = [
|
|
8380
|
+
"@radix-ui/react-dialog",
|
|
8381
|
+
"@radix-ui/react-dropdown-menu",
|
|
8382
|
+
"@radix-ui/react-label",
|
|
8383
|
+
"@radix-ui/react-select",
|
|
8384
|
+
"@radix-ui/react-separator",
|
|
8385
|
+
"@radix-ui/react-toast"
|
|
8386
|
+
];
|
|
8387
|
+
packages.push(...radixPrimitives);
|
|
8388
|
+
await installPackages(packages, {
|
|
8389
|
+
dev: false,
|
|
8390
|
+
packageManager: ctx.packageManager,
|
|
8391
|
+
projectRoot: ctx.projectRoot,
|
|
8392
|
+
exact: false,
|
|
8393
|
+
silent: false
|
|
8394
|
+
});
|
|
8395
|
+
logger.info("Successfully installed Shadcn/ui dependencies");
|
|
8396
|
+
return {
|
|
8397
|
+
packages: {
|
|
8398
|
+
dependencies: packages
|
|
8399
|
+
},
|
|
8400
|
+
success: true,
|
|
8401
|
+
message: `Installed Shadcn/ui dependencies: ${packages.join(", ")}`
|
|
8402
|
+
};
|
|
8403
|
+
} catch (error) {
|
|
8404
|
+
logger.error("Failed to install Shadcn/ui dependencies:", error);
|
|
8405
|
+
return {
|
|
8406
|
+
packages: {},
|
|
8407
|
+
success: false,
|
|
8408
|
+
message: `Failed to install Shadcn/ui dependencies: ${error instanceof Error ? error.message : String(error)}`
|
|
8409
|
+
};
|
|
8410
|
+
}
|
|
8411
|
+
},
|
|
8412
|
+
/**
|
|
8413
|
+
* Configure Shadcn/ui dans le projet Next.js
|
|
8414
|
+
*
|
|
8415
|
+
* Crée :
|
|
8416
|
+
* - components.json : Configuration Shadcn/ui adaptée Next.js
|
|
8417
|
+
* - lib/utils.ts : Utilitaires (cn helper) - adapté pour Next.js
|
|
8418
|
+
* - components/ui/button.tsx : Exemple de composant Button
|
|
8419
|
+
*/
|
|
8420
|
+
async configure(ctx) {
|
|
8421
|
+
const backupManager = new BackupManager();
|
|
8422
|
+
const writer = new ConfigWriter(backupManager);
|
|
8423
|
+
const files = [];
|
|
8424
|
+
const projectRoot = ctx.projectRoot;
|
|
8425
|
+
const hasSrcDir = ctx.srcDir && ctx.srcDir !== ".";
|
|
8426
|
+
const baseDir = hasSrcDir ? join25(projectRoot, ctx.srcDir) : projectRoot;
|
|
8427
|
+
try {
|
|
8428
|
+
const componentsJsonPath = join25(projectRoot, "components.json");
|
|
8429
|
+
const componentsJsonExists = await checkPathExists(componentsJsonPath);
|
|
8430
|
+
if (componentsJsonExists) {
|
|
8431
|
+
logger.warn("components.json already exists, skipping creation");
|
|
8432
|
+
} else {
|
|
8433
|
+
const componentsJsonContent = getComponentsJsonContentNextjs(ctx);
|
|
8434
|
+
await writer.createFile(componentsJsonPath, componentsJsonContent);
|
|
8435
|
+
files.push({
|
|
8436
|
+
type: "create",
|
|
8437
|
+
path: normalizePath(componentsJsonPath),
|
|
8438
|
+
content: componentsJsonContent,
|
|
8439
|
+
backup: false
|
|
8440
|
+
});
|
|
8441
|
+
logger.info(`Created components.json: ${componentsJsonPath}`);
|
|
8442
|
+
}
|
|
8443
|
+
const libDir = join25(baseDir, "lib");
|
|
8444
|
+
await ensureDirectory(libDir);
|
|
8445
|
+
const utilsPath = join25(libDir, `utils.${ctx.typescript ? "ts" : "js"}`);
|
|
8446
|
+
const utilsExists = await checkPathExists(utilsPath);
|
|
8447
|
+
if (utilsExists) {
|
|
8448
|
+
logger.warn(
|
|
8449
|
+
"utils.ts already exists, checking if cn function is present"
|
|
8450
|
+
);
|
|
8451
|
+
const existingContent = await readFileContent(utilsPath);
|
|
8452
|
+
if (!existingContent.includes("export function cn")) {
|
|
8453
|
+
const cnFunction = ctx.typescript ? getCnFunctionTS2() : getCnFunctionJS2();
|
|
8454
|
+
const updatedContent = existingContent + "\n\n" + cnFunction;
|
|
8455
|
+
await writer.writeFile(utilsPath, updatedContent, { backup: true });
|
|
8456
|
+
files.push({
|
|
8457
|
+
type: "modify",
|
|
8458
|
+
path: normalizePath(utilsPath),
|
|
8459
|
+
content: updatedContent,
|
|
8460
|
+
backup: true
|
|
8461
|
+
});
|
|
8462
|
+
logger.info("Added cn function to utils.ts");
|
|
8463
|
+
}
|
|
8464
|
+
} else {
|
|
8465
|
+
const utilsContent = ctx.typescript ? getUtilsContentTS2() : getUtilsContentJS2();
|
|
8466
|
+
await writer.createFile(utilsPath, utilsContent);
|
|
8467
|
+
files.push({
|
|
8468
|
+
type: "create",
|
|
8469
|
+
path: normalizePath(utilsPath),
|
|
8470
|
+
content: utilsContent,
|
|
8471
|
+
backup: false
|
|
8472
|
+
});
|
|
8473
|
+
logger.info(`Created utils file: ${utilsPath}`);
|
|
8474
|
+
}
|
|
8475
|
+
const uiDir = join25(baseDir, "components", "ui");
|
|
8476
|
+
await ensureDirectory(uiDir);
|
|
8477
|
+
const buttonPath = join25(uiDir, `button.${ctx.typescript ? "tsx" : "jsx"}`);
|
|
8478
|
+
const buttonExists = await checkPathExists(buttonPath);
|
|
8479
|
+
if (!buttonExists) {
|
|
8480
|
+
const buttonContent = ctx.typescript ? getButtonContentTS4() : getButtonContentJS4();
|
|
8481
|
+
await writer.createFile(buttonPath, buttonContent);
|
|
8482
|
+
files.push({
|
|
8483
|
+
type: "create",
|
|
8484
|
+
path: normalizePath(buttonPath),
|
|
8485
|
+
content: buttonContent,
|
|
8486
|
+
backup: false
|
|
8487
|
+
});
|
|
8488
|
+
logger.info(`Created Button component: ${buttonPath}`);
|
|
8489
|
+
}
|
|
8490
|
+
const cssFiles = [
|
|
8491
|
+
join25(projectRoot, "app", "globals.css"),
|
|
8492
|
+
join25(baseDir, "app", "globals.css"),
|
|
8493
|
+
join25(projectRoot, "styles", "globals.css"),
|
|
8494
|
+
join25(baseDir, "styles", "globals.css")
|
|
8495
|
+
];
|
|
8496
|
+
for (const cssPath of cssFiles) {
|
|
8497
|
+
const cssExists = await checkPathExists(cssPath);
|
|
8498
|
+
if (cssExists) {
|
|
8499
|
+
const cssContent = await readFileContent(cssPath);
|
|
8500
|
+
if (!cssContent.includes("@layer base")) {
|
|
8501
|
+
const shadcnVariables = getShadcnCSSVariables2();
|
|
8502
|
+
const updatedCss = cssContent + "\n\n" + shadcnVariables;
|
|
8503
|
+
await writer.writeFile(cssPath, updatedCss, { backup: true });
|
|
8504
|
+
files.push({
|
|
8505
|
+
type: "modify",
|
|
8506
|
+
path: normalizePath(cssPath),
|
|
8507
|
+
content: updatedCss,
|
|
8508
|
+
backup: true
|
|
8509
|
+
});
|
|
8510
|
+
logger.info(`Added Shadcn/ui CSS variables to ${cssPath}`);
|
|
8511
|
+
}
|
|
8512
|
+
break;
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
return {
|
|
8516
|
+
files,
|
|
8517
|
+
success: true,
|
|
8518
|
+
message: "Shadcn/ui configured successfully for Next.js"
|
|
8519
|
+
};
|
|
8520
|
+
} catch (error) {
|
|
8521
|
+
logger.error("Failed to configure Shadcn/ui:", error);
|
|
8522
|
+
return {
|
|
8523
|
+
files,
|
|
8524
|
+
success: false,
|
|
8525
|
+
message: `Failed to configure Shadcn/ui: ${error instanceof Error ? error.message : String(error)}`
|
|
8526
|
+
};
|
|
8527
|
+
}
|
|
8528
|
+
},
|
|
8529
|
+
/**
|
|
8530
|
+
* Rollback de la configuration Shadcn/ui
|
|
8531
|
+
*/
|
|
8532
|
+
async rollback(_ctx) {
|
|
8533
|
+
const backupManager = new BackupManager();
|
|
8534
|
+
try {
|
|
8535
|
+
await backupManager.restoreAll();
|
|
8536
|
+
logger.info("Shadcn/ui configuration rolled back");
|
|
8537
|
+
} catch (error) {
|
|
8538
|
+
logger.error("Failed to rollback Shadcn/ui configuration:", error);
|
|
8539
|
+
throw error;
|
|
8540
|
+
}
|
|
8541
|
+
}
|
|
8542
|
+
};
|
|
8543
|
+
function getComponentsJsonContentNextjs(ctx) {
|
|
8544
|
+
const hasSrcDir = ctx.srcDir && ctx.srcDir !== ".";
|
|
8545
|
+
const prefix = hasSrcDir ? `${ctx.srcDir}/` : "";
|
|
8546
|
+
const style = "new-york";
|
|
8547
|
+
const tailwindConfig = "tailwind.config.js";
|
|
8548
|
+
const tailwindCss = hasSrcDir ? `${ctx.srcDir}/app/globals.css` : "app/globals.css";
|
|
8549
|
+
const components = `${prefix}components`;
|
|
8550
|
+
const utils = `${prefix}lib/utils`;
|
|
8551
|
+
return JSON.stringify(
|
|
8552
|
+
{
|
|
8553
|
+
$schema: "https://ui.shadcn.com/schema.json",
|
|
8554
|
+
style,
|
|
8555
|
+
rsc: true,
|
|
8556
|
+
// React Server Components pour Next.js
|
|
8557
|
+
tsx: ctx.typescript,
|
|
8558
|
+
tailwind: {
|
|
8559
|
+
config: tailwindConfig,
|
|
8560
|
+
css: tailwindCss,
|
|
8561
|
+
baseColor: "slate",
|
|
8562
|
+
cssVariables: true,
|
|
8563
|
+
prefix: ""
|
|
8564
|
+
},
|
|
8565
|
+
aliases: {
|
|
8566
|
+
components,
|
|
8567
|
+
utils,
|
|
8568
|
+
ui: `${components}/ui`,
|
|
8569
|
+
lib: `${prefix}lib`,
|
|
8570
|
+
hooks: `${prefix}hooks`
|
|
8571
|
+
}
|
|
8572
|
+
},
|
|
8573
|
+
null,
|
|
8574
|
+
2
|
|
8575
|
+
);
|
|
8576
|
+
}
|
|
8577
|
+
function getUtilsContentTS2() {
|
|
8578
|
+
return `import { type ClassValue, clsx } from 'clsx'
|
|
8579
|
+
import { twMerge } from 'tailwind-merge'
|
|
8580
|
+
|
|
8581
|
+
/**
|
|
8582
|
+
* Fonction utilitaire pour fusionner les classes TailwindCSS
|
|
8583
|
+
*
|
|
8584
|
+
* Utilise clsx pour g\xE9rer les conditions et twMerge pour r\xE9soudre les conflits
|
|
8585
|
+
*
|
|
8586
|
+
* @example
|
|
8587
|
+
* \`\`\`tsx
|
|
8588
|
+
* import { cn } from '@/lib/utils'
|
|
8589
|
+
*
|
|
8590
|
+
* <div className={cn('px-2 py-1', isActive && 'bg-blue-500')} />
|
|
8591
|
+
* \`\`\`
|
|
8592
|
+
*/
|
|
8593
|
+
export function cn(...inputs: ClassValue[]) {
|
|
8594
|
+
return twMerge(clsx(inputs))
|
|
8595
|
+
}
|
|
8596
|
+
`;
|
|
8597
|
+
}
|
|
8598
|
+
function getUtilsContentJS2() {
|
|
8599
|
+
return `import { clsx } from 'clsx'
|
|
8600
|
+
import { twMerge } from 'tailwind-merge'
|
|
8601
|
+
|
|
8602
|
+
/**
|
|
8603
|
+
* Fonction utilitaire pour fusionner les classes TailwindCSS
|
|
8604
|
+
*
|
|
8605
|
+
* Utilise clsx pour g\xE9rer les conditions et twMerge pour r\xE9soudre les conflits
|
|
8606
|
+
*
|
|
8607
|
+
* @example
|
|
8608
|
+
* \`\`\`jsx
|
|
8609
|
+
* import { cn } from '@/lib/utils'
|
|
8610
|
+
*
|
|
8611
|
+
* <div className={cn('px-2 py-1', isActive && 'bg-blue-500')} />
|
|
8612
|
+
* \`\`\`
|
|
8613
|
+
*/
|
|
8614
|
+
export function cn(...inputs) {
|
|
8615
|
+
return twMerge(clsx(inputs))
|
|
8616
|
+
}
|
|
8617
|
+
`;
|
|
8618
|
+
}
|
|
8619
|
+
function getCnFunctionTS2() {
|
|
8620
|
+
return `import { type ClassValue, clsx } from 'clsx'
|
|
8621
|
+
import { twMerge } from 'tailwind-merge'
|
|
8622
|
+
|
|
8623
|
+
export function cn(...inputs: ClassValue[]) {
|
|
8624
|
+
return twMerge(clsx(inputs))
|
|
8625
|
+
}
|
|
8626
|
+
`;
|
|
8627
|
+
}
|
|
8628
|
+
function getCnFunctionJS2() {
|
|
8629
|
+
return `import { clsx } from 'clsx'
|
|
8630
|
+
import { twMerge } from 'tailwind-merge'
|
|
8631
|
+
|
|
8632
|
+
export function cn(...inputs) {
|
|
8633
|
+
return twMerge(clsx(inputs))
|
|
8634
|
+
}
|
|
8635
|
+
`;
|
|
8636
|
+
}
|
|
8637
|
+
function getButtonContentTS4() {
|
|
8638
|
+
return `import * as React from 'react'
|
|
8639
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
8640
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
8641
|
+
|
|
8642
|
+
import { cn } from '@/lib/utils'
|
|
8643
|
+
|
|
8644
|
+
const buttonVariants = cva(
|
|
8645
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
8646
|
+
{
|
|
8647
|
+
variants: {
|
|
8648
|
+
variant: {
|
|
8649
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
8650
|
+
destructive:
|
|
8651
|
+
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
8652
|
+
outline:
|
|
8653
|
+
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
8654
|
+
secondary:
|
|
8655
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
8656
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
8657
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
8658
|
+
},
|
|
8659
|
+
size: {
|
|
8660
|
+
default: 'h-10 px-4 py-2',
|
|
8661
|
+
sm: 'h-9 rounded-md px-3',
|
|
8662
|
+
lg: 'h-11 rounded-md px-8',
|
|
8663
|
+
icon: 'h-10 w-10',
|
|
8664
|
+
},
|
|
8665
|
+
},
|
|
8666
|
+
defaultVariants: {
|
|
8667
|
+
variant: 'default',
|
|
8668
|
+
size: 'default',
|
|
8669
|
+
},
|
|
8670
|
+
}
|
|
8671
|
+
)
|
|
8672
|
+
|
|
8673
|
+
export interface ButtonProps
|
|
8674
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
8675
|
+
VariantProps<typeof buttonVariants> {
|
|
8676
|
+
asChild?: boolean
|
|
8677
|
+
}
|
|
8678
|
+
|
|
8679
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
8680
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
8681
|
+
const Comp = asChild ? Slot : 'button'
|
|
8682
|
+
return (
|
|
8683
|
+
<Comp
|
|
8684
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
8685
|
+
ref={ref}
|
|
8686
|
+
{...props}
|
|
8687
|
+
/>
|
|
8688
|
+
)
|
|
8689
|
+
}
|
|
8690
|
+
)
|
|
8691
|
+
Button.displayName = 'Button'
|
|
8692
|
+
|
|
8693
|
+
export { Button, buttonVariants }
|
|
8694
|
+
`;
|
|
8695
|
+
}
|
|
8696
|
+
function getButtonContentJS4() {
|
|
8697
|
+
return `import * as React from 'react'
|
|
8698
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
8699
|
+
import { cva } from 'class-variance-authority'
|
|
8700
|
+
|
|
8701
|
+
import { cn } from '@/lib/utils'
|
|
8702
|
+
|
|
8703
|
+
const buttonVariants = cva(
|
|
8704
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
8705
|
+
{
|
|
8706
|
+
variants: {
|
|
8707
|
+
variant: {
|
|
8708
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
8709
|
+
destructive:
|
|
8710
|
+
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
8711
|
+
outline:
|
|
8712
|
+
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
8713
|
+
secondary:
|
|
8714
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
8715
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
8716
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
8717
|
+
},
|
|
8718
|
+
size: {
|
|
8719
|
+
default: 'h-10 px-4 py-2',
|
|
8720
|
+
sm: 'h-9 rounded-md px-3',
|
|
8721
|
+
lg: 'h-11 rounded-md px-8',
|
|
8722
|
+
icon: 'h-10 w-10',
|
|
8723
|
+
},
|
|
8724
|
+
},
|
|
8725
|
+
defaultVariants: {
|
|
8726
|
+
variant: 'default',
|
|
8727
|
+
size: 'default',
|
|
8728
|
+
},
|
|
8729
|
+
}
|
|
8730
|
+
)
|
|
8731
|
+
|
|
8732
|
+
const Button = React.forwardRef(
|
|
8733
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
8734
|
+
const Comp = asChild ? Slot : 'button'
|
|
8735
|
+
return (
|
|
8736
|
+
<Comp
|
|
8737
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
8738
|
+
ref={ref}
|
|
8739
|
+
{...props}
|
|
8740
|
+
/>
|
|
8741
|
+
)
|
|
8742
|
+
}
|
|
8743
|
+
)
|
|
8744
|
+
Button.displayName = 'Button'
|
|
8745
|
+
|
|
8746
|
+
export { Button, buttonVariants }
|
|
8747
|
+
`;
|
|
8748
|
+
}
|
|
8749
|
+
function getShadcnCSSVariables2() {
|
|
8750
|
+
return `@layer base {
|
|
8751
|
+
:root {
|
|
8752
|
+
--background: 0 0% 100%;
|
|
8753
|
+
--foreground: 222.2 84% 4.9%;
|
|
8754
|
+
--card: 0 0% 100%;
|
|
8755
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
8756
|
+
--popover: 0 0% 100%;
|
|
8757
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
8758
|
+
--primary: 222.2 47.4% 11.2%;
|
|
8759
|
+
--primary-foreground: 210 40% 98%;
|
|
8760
|
+
--secondary: 210 40% 96.1%;
|
|
8761
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
8762
|
+
--muted: 210 40% 96.1%;
|
|
8763
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
8764
|
+
--accent: 210 40% 96.1%;
|
|
8765
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
8766
|
+
--destructive: 0 84.2% 60.2%;
|
|
8767
|
+
--destructive-foreground: 210 40% 98%;
|
|
8768
|
+
--border: 214.3 31.8% 91.4%;
|
|
8769
|
+
--input: 214.3 31.8% 91.4%;
|
|
8770
|
+
--ring: 222.2 84% 4.9%;
|
|
8771
|
+
--radius: 0.5rem;
|
|
8772
|
+
}
|
|
8773
|
+
|
|
8774
|
+
.dark {
|
|
8775
|
+
--background: 222.2 84% 4.9%;
|
|
8776
|
+
--foreground: 210 40% 98%;
|
|
8777
|
+
--card: 222.2 84% 4.9%;
|
|
8778
|
+
--card-foreground: 210 40% 98%;
|
|
8779
|
+
--popover: 222.2 84% 4.9%;
|
|
8780
|
+
--popover-foreground: 210 40% 98%;
|
|
8781
|
+
--primary: 210 40% 98%;
|
|
8782
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
8783
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
8784
|
+
--secondary-foreground: 210 40% 98%;
|
|
8785
|
+
--muted: 217.2 32.6% 17.5%;
|
|
8786
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
8787
|
+
--accent: 217.2 32.6% 17.5%;
|
|
8788
|
+
--accent-foreground: 210 40% 98%;
|
|
8789
|
+
--destructive: 0 62.8% 30.6%;
|
|
8790
|
+
--destructive-foreground: 210 40% 98%;
|
|
8791
|
+
--border: 217.2 32.6% 17.5%;
|
|
8792
|
+
--input: 217.2 32.6% 17.5%;
|
|
8793
|
+
--ring: 212.7 26.8% 83.9%;
|
|
8794
|
+
}
|
|
8795
|
+
}
|
|
8796
|
+
|
|
8797
|
+
@layer base {
|
|
8798
|
+
* {
|
|
8799
|
+
@apply border-border;
|
|
8800
|
+
}
|
|
8801
|
+
body {
|
|
8802
|
+
@apply bg-background text-foreground;
|
|
8803
|
+
}
|
|
8804
|
+
}
|
|
8805
|
+
`;
|
|
8806
|
+
}
|
|
8807
|
+
|
|
8808
|
+
// src/plugins/ui/react-hot-toast-nextjs.ts
|
|
8809
|
+
import { join as join26 } from "path";
|
|
8810
|
+
var reactHotToastNextjsPlugin = {
|
|
8811
|
+
name: "react-hot-toast-nextjs",
|
|
8812
|
+
displayName: "React Hot Toast (Next.js)",
|
|
8813
|
+
description: "Notifications toast pour Next.js",
|
|
8814
|
+
category: "ui" /* UI */,
|
|
8815
|
+
version: "^2.4.1",
|
|
8816
|
+
frameworks: ["nextjs"],
|
|
8817
|
+
/**
|
|
8818
|
+
* Détecte si React Hot Toast est déjà installé
|
|
8819
|
+
*/
|
|
8820
|
+
detect: (ctx) => {
|
|
8821
|
+
return ctx.dependencies["react-hot-toast"] !== void 0 || ctx.devDependencies["react-hot-toast"] !== void 0;
|
|
8822
|
+
},
|
|
8823
|
+
/**
|
|
8824
|
+
* Installe React Hot Toast
|
|
8825
|
+
*/
|
|
8826
|
+
async install(ctx) {
|
|
8827
|
+
if (this.detect?.(ctx)) {
|
|
8828
|
+
logger.info("React Hot Toast is already installed");
|
|
8829
|
+
return {
|
|
8830
|
+
packages: {},
|
|
8831
|
+
success: true,
|
|
8832
|
+
message: "React Hot Toast already installed"
|
|
8833
|
+
};
|
|
8834
|
+
}
|
|
8835
|
+
try {
|
|
8836
|
+
const packages = ["react-hot-toast"];
|
|
8837
|
+
await installPackages(packages, {
|
|
8838
|
+
dev: false,
|
|
8839
|
+
packageManager: ctx.packageManager,
|
|
8840
|
+
projectRoot: ctx.projectRoot,
|
|
8841
|
+
exact: false,
|
|
8842
|
+
silent: false
|
|
8843
|
+
});
|
|
8844
|
+
logger.info("Successfully installed React Hot Toast");
|
|
8845
|
+
return {
|
|
8846
|
+
packages: {
|
|
8847
|
+
dependencies: packages
|
|
8848
|
+
},
|
|
8849
|
+
success: true,
|
|
8850
|
+
message: `Installed React Hot Toast: ${packages.join(", ")}`
|
|
8851
|
+
};
|
|
8852
|
+
} catch (error) {
|
|
8853
|
+
logger.error("Failed to install React Hot Toast:", error);
|
|
8854
|
+
return {
|
|
8855
|
+
packages: {},
|
|
8856
|
+
success: false,
|
|
8857
|
+
message: `Failed to install React Hot Toast: ${error instanceof Error ? error.message : String(error)}`
|
|
8858
|
+
};
|
|
8859
|
+
}
|
|
8860
|
+
},
|
|
8861
|
+
/**
|
|
8862
|
+
* Configure React Hot Toast dans le projet Next.js
|
|
8863
|
+
*
|
|
8864
|
+
* Ajoute :
|
|
8865
|
+
* - Toaster dans app/layout.tsx (App Router) ou pages/_app.tsx (Pages Router)
|
|
8866
|
+
*/
|
|
8867
|
+
async configure(ctx) {
|
|
8868
|
+
const backupManager = new BackupManager();
|
|
8869
|
+
const writer = new ConfigWriter(backupManager);
|
|
8870
|
+
const files = [];
|
|
8871
|
+
const projectRoot = ctx.projectRoot;
|
|
8872
|
+
const extension = ctx.typescript ? "tsx" : "jsx";
|
|
8873
|
+
const hasSrcDir = ctx.srcDir && ctx.srcDir !== ".";
|
|
8874
|
+
try {
|
|
8875
|
+
const isAppRouter = ctx.nextjsRouter === "app" || ctx.nextjsRouter === void 0 && await checkPathExists(
|
|
8876
|
+
hasSrcDir ? join26(projectRoot, ctx.srcDir, "app") : join26(projectRoot, "app")
|
|
8877
|
+
);
|
|
8878
|
+
const appLayoutPath = hasSrcDir ? join26(projectRoot, ctx.srcDir, "app", `layout.${extension}`) : join26(projectRoot, "app", `layout.${extension}`);
|
|
8879
|
+
const pagesAppPath = hasSrcDir ? join26(projectRoot, ctx.srcDir, "pages", `_app.${extension}`) : join26(projectRoot, "pages", `_app.${extension}`);
|
|
8880
|
+
let targetPath = null;
|
|
8881
|
+
let targetContent = "";
|
|
8882
|
+
if (isAppRouter) {
|
|
8883
|
+
const appLayoutExists = await checkPathExists(appLayoutPath);
|
|
8884
|
+
if (appLayoutExists) {
|
|
8885
|
+
targetPath = appLayoutPath;
|
|
8886
|
+
targetContent = await readFileContent(appLayoutPath);
|
|
8887
|
+
} else {
|
|
8888
|
+
targetPath = appLayoutPath;
|
|
8889
|
+
targetContent = ctx.typescript ? getAppLayoutContentTS() : getAppLayoutContentJS();
|
|
8890
|
+
}
|
|
8891
|
+
} else {
|
|
8892
|
+
const pagesAppExists = await checkPathExists(pagesAppPath);
|
|
8893
|
+
if (pagesAppExists) {
|
|
8894
|
+
targetPath = pagesAppPath;
|
|
8895
|
+
targetContent = await readFileContent(pagesAppPath);
|
|
8896
|
+
} else {
|
|
8897
|
+
targetPath = pagesAppPath;
|
|
8898
|
+
targetContent = ctx.typescript ? getPagesAppContentTS() : getPagesAppContentJS();
|
|
8899
|
+
}
|
|
8900
|
+
}
|
|
8901
|
+
if (targetPath) {
|
|
8902
|
+
const hasImport = targetContent.includes("react-hot-toast");
|
|
8903
|
+
const hasToaster = targetContent.includes("<Toaster />");
|
|
8904
|
+
if (!hasImport || !hasToaster) {
|
|
8905
|
+
const updatedContent = injectToaster2(
|
|
8906
|
+
targetContent,
|
|
8907
|
+
ctx.typescript,
|
|
8908
|
+
isAppRouter
|
|
8909
|
+
);
|
|
8910
|
+
await writer.writeFile(targetPath, updatedContent, { backup: true });
|
|
8911
|
+
files.push({
|
|
8912
|
+
type: targetContent ? "modify" : "create",
|
|
8913
|
+
path: normalizePath(targetPath),
|
|
8914
|
+
content: updatedContent,
|
|
8915
|
+
backup: targetContent ? true : false
|
|
8916
|
+
});
|
|
8917
|
+
logger.info(`Added Toaster to ${targetPath}`);
|
|
8918
|
+
} else {
|
|
8919
|
+
logger.warn("Toaster already configured");
|
|
8920
|
+
}
|
|
8921
|
+
}
|
|
8922
|
+
return {
|
|
8923
|
+
files,
|
|
8924
|
+
success: true,
|
|
8925
|
+
message: "React Hot Toast configured successfully for Next.js"
|
|
8926
|
+
};
|
|
8927
|
+
} catch (error) {
|
|
8928
|
+
logger.error("Failed to configure React Hot Toast:", error);
|
|
8929
|
+
await backupManager.restoreAll();
|
|
8930
|
+
return {
|
|
8931
|
+
files,
|
|
8932
|
+
success: false,
|
|
8933
|
+
message: `Failed to configure React Hot Toast: ${error instanceof Error ? error.message : String(error)}`
|
|
8934
|
+
};
|
|
8935
|
+
}
|
|
8936
|
+
},
|
|
8937
|
+
/**
|
|
8938
|
+
* Rollback de la configuration React Hot Toast
|
|
8939
|
+
*/
|
|
8940
|
+
async rollback(_ctx) {
|
|
8941
|
+
const backupManager = new BackupManager();
|
|
8942
|
+
try {
|
|
8943
|
+
await backupManager.restoreAll();
|
|
8944
|
+
logger.info("React Hot Toast configuration rolled back");
|
|
8945
|
+
} catch (error) {
|
|
8946
|
+
logger.error("Failed to rollback React Hot Toast configuration:", error);
|
|
8947
|
+
throw error;
|
|
8948
|
+
}
|
|
8949
|
+
}
|
|
8950
|
+
};
|
|
8951
|
+
function injectToaster2(content, _isTypeScript, isAppRouter) {
|
|
8952
|
+
if (content.includes("<Toaster />") || content.includes("<Toaster/>")) {
|
|
8953
|
+
return content;
|
|
8954
|
+
}
|
|
8955
|
+
let modifiedContent = content;
|
|
8956
|
+
if (!content.includes("react-hot-toast")) {
|
|
8957
|
+
const importStatement = "import { Toaster } from 'react-hot-toast'\n";
|
|
8958
|
+
const lines = modifiedContent.split("\n");
|
|
8959
|
+
let lastImportIndex = -1;
|
|
8960
|
+
for (let i = 0; i < lines.length; i++) {
|
|
8961
|
+
if (lines[i]?.startsWith("import ")) {
|
|
8962
|
+
lastImportIndex = i;
|
|
8963
|
+
}
|
|
8964
|
+
}
|
|
8965
|
+
if (lastImportIndex >= 0) {
|
|
8966
|
+
lines.splice(lastImportIndex + 1, 0, importStatement.trim());
|
|
8967
|
+
modifiedContent = lines.join("\n");
|
|
8968
|
+
} else {
|
|
8969
|
+
modifiedContent = importStatement + modifiedContent;
|
|
8970
|
+
}
|
|
8971
|
+
}
|
|
8972
|
+
if (isAppRouter) {
|
|
8973
|
+
if (modifiedContent.includes("return (")) {
|
|
8974
|
+
modifiedContent = modifiedContent.replace(
|
|
8975
|
+
/(return\s*\([\s\S]*?<body[^>]*>)/,
|
|
8976
|
+
(match) => `${match}
|
|
8977
|
+
<Toaster />`
|
|
8978
|
+
);
|
|
8979
|
+
} else if (modifiedContent.includes("<body")) {
|
|
8980
|
+
modifiedContent = modifiedContent.replace(
|
|
8981
|
+
/(<body[^>]*>)/,
|
|
8982
|
+
(match) => `${match}
|
|
8983
|
+
<Toaster />`
|
|
8984
|
+
);
|
|
8985
|
+
} else {
|
|
8986
|
+
modifiedContent = modifiedContent.replace(
|
|
8987
|
+
/(\s*)(<\/[^>]+>\s*)$/,
|
|
8988
|
+
(match, indent) => `${indent}<Toaster />
|
|
8989
|
+
${match}`
|
|
8990
|
+
);
|
|
8991
|
+
}
|
|
8992
|
+
} else {
|
|
8993
|
+
if (modifiedContent.includes("return (")) {
|
|
8994
|
+
modifiedContent = modifiedContent.replace(
|
|
8995
|
+
/(return\s*\([\s\S]*?<)/,
|
|
8996
|
+
(match) => `${match}<Toaster />
|
|
8997
|
+
`
|
|
8998
|
+
);
|
|
8999
|
+
} else {
|
|
9000
|
+
modifiedContent = modifiedContent.replace(
|
|
9001
|
+
/(\s*)(<\/[^>]+>\s*)$/,
|
|
9002
|
+
(match, indent) => `${indent}<Toaster />
|
|
9003
|
+
${match}`
|
|
9004
|
+
);
|
|
9005
|
+
}
|
|
9006
|
+
}
|
|
9007
|
+
return modifiedContent;
|
|
9008
|
+
}
|
|
9009
|
+
function getAppLayoutContentTS() {
|
|
9010
|
+
return `import type { Metadata } from 'next'
|
|
9011
|
+
import { Inter } from 'next/font/google'
|
|
9012
|
+
import { Toaster } from 'react-hot-toast'
|
|
9013
|
+
import './globals.css'
|
|
9014
|
+
|
|
9015
|
+
const inter = Inter({ subsets: ['latin'] })
|
|
9016
|
+
|
|
9017
|
+
export const metadata: Metadata = {
|
|
9018
|
+
title: 'Create Next App',
|
|
9019
|
+
description: 'Generated by create next app',
|
|
9020
|
+
}
|
|
9021
|
+
|
|
9022
|
+
export default function RootLayout({
|
|
9023
|
+
children,
|
|
9024
|
+
}: {
|
|
9025
|
+
children: React.ReactNode
|
|
9026
|
+
}) {
|
|
9027
|
+
return (
|
|
9028
|
+
<html lang="en">
|
|
9029
|
+
<body className={inter.className}>
|
|
9030
|
+
<Toaster />
|
|
9031
|
+
{children}
|
|
9032
|
+
</body>
|
|
9033
|
+
</html>
|
|
9034
|
+
)
|
|
9035
|
+
}
|
|
9036
|
+
`;
|
|
9037
|
+
}
|
|
9038
|
+
function getAppLayoutContentJS() {
|
|
9039
|
+
return `import { Inter } from 'next/font/google'
|
|
9040
|
+
import { Toaster } from 'react-hot-toast'
|
|
9041
|
+
import './globals.css'
|
|
9042
|
+
|
|
9043
|
+
const inter = Inter({ subsets: ['latin'] })
|
|
9044
|
+
|
|
9045
|
+
export const metadata = {
|
|
9046
|
+
title: 'Create Next App',
|
|
9047
|
+
description: 'Generated by create next app',
|
|
9048
|
+
}
|
|
9049
|
+
|
|
9050
|
+
export default function RootLayout({ children }) {
|
|
9051
|
+
return (
|
|
9052
|
+
<html lang="en">
|
|
9053
|
+
<body className={inter.className}>
|
|
9054
|
+
<Toaster />
|
|
9055
|
+
{children}
|
|
9056
|
+
</body>
|
|
9057
|
+
</html>
|
|
9058
|
+
)
|
|
9059
|
+
}
|
|
9060
|
+
`;
|
|
9061
|
+
}
|
|
9062
|
+
function getPagesAppContentTS() {
|
|
9063
|
+
return `import type { AppProps } from 'next/app'
|
|
9064
|
+
import { Toaster } from 'react-hot-toast'
|
|
9065
|
+
import '../styles/globals.css'
|
|
9066
|
+
|
|
9067
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
9068
|
+
return (
|
|
9069
|
+
<>
|
|
9070
|
+
<Toaster />
|
|
9071
|
+
<Component {...pageProps} />
|
|
9072
|
+
</>
|
|
9073
|
+
)
|
|
9074
|
+
}
|
|
9075
|
+
`;
|
|
9076
|
+
}
|
|
9077
|
+
function getPagesAppContentJS() {
|
|
9078
|
+
return `import { Toaster } from 'react-hot-toast'
|
|
9079
|
+
import '../styles/globals.css'
|
|
9080
|
+
|
|
9081
|
+
export default function App({ Component, pageProps }) {
|
|
9082
|
+
return (
|
|
9083
|
+
<>
|
|
9084
|
+
<Toaster />
|
|
9085
|
+
<Component {...pageProps} />
|
|
9086
|
+
</>
|
|
9087
|
+
)
|
|
9088
|
+
}
|
|
9089
|
+
`;
|
|
9090
|
+
}
|
|
9091
|
+
|
|
9092
|
+
// src/plugins/nextjs/image-optimization.ts
|
|
9093
|
+
import { join as join27 } from "path";
|
|
9094
|
+
var nextjsImageOptimizationPlugin = {
|
|
9095
|
+
name: "nextjs-image-optimization",
|
|
9096
|
+
displayName: "Next.js Image Optimization",
|
|
9097
|
+
description: "Configuration de l'optimisation d'images pour Next.js",
|
|
9098
|
+
category: "tooling" /* TOOLING */,
|
|
9099
|
+
version: void 0,
|
|
9100
|
+
frameworks: ["nextjs"],
|
|
9101
|
+
/**
|
|
9102
|
+
* Détecte si l'optimisation d'images est déjà configurée
|
|
9103
|
+
*/
|
|
9104
|
+
detect: (ctx) => {
|
|
9105
|
+
return ctx.dependencies["next"] !== void 0 || ctx.devDependencies["next"] !== void 0;
|
|
9106
|
+
},
|
|
9107
|
+
/**
|
|
9108
|
+
* Pas d'installation nécessaire, Next.js inclut déjà l'optimisation d'images
|
|
9109
|
+
*/
|
|
9110
|
+
install(_ctx) {
|
|
9111
|
+
logger.info(
|
|
9112
|
+
"Image optimization is built into Next.js, no installation needed"
|
|
9113
|
+
);
|
|
9114
|
+
return Promise.resolve({
|
|
9115
|
+
packages: {},
|
|
9116
|
+
success: true,
|
|
9117
|
+
message: "Image optimization is built into Next.js"
|
|
9118
|
+
});
|
|
9119
|
+
},
|
|
9120
|
+
/**
|
|
9121
|
+
* Configure l'optimisation d'images dans next.config.js
|
|
9122
|
+
*/
|
|
9123
|
+
async configure(ctx) {
|
|
9124
|
+
const backupManager = new BackupManager();
|
|
9125
|
+
const writer = new ConfigWriter(backupManager);
|
|
9126
|
+
const files = [];
|
|
9127
|
+
const projectRoot = ctx.projectRoot;
|
|
9128
|
+
const extension = ctx.typescript ? "ts" : "js";
|
|
9129
|
+
try {
|
|
9130
|
+
const nextConfigPath = join27(projectRoot, `next.config.${extension}`);
|
|
9131
|
+
const nextConfigExists = await checkPathExists(nextConfigPath);
|
|
9132
|
+
if (nextConfigExists) {
|
|
9133
|
+
const existingContent = await readFileContent(nextConfigPath);
|
|
9134
|
+
const updatedContent = injectImageConfig(existingContent, extension);
|
|
9135
|
+
if (updatedContent !== existingContent) {
|
|
9136
|
+
await writer.writeFile(nextConfigPath, updatedContent, {
|
|
9137
|
+
backup: true
|
|
9138
|
+
});
|
|
9139
|
+
files.push({
|
|
9140
|
+
type: "modify",
|
|
9141
|
+
path: normalizePath(nextConfigPath),
|
|
9142
|
+
content: updatedContent,
|
|
9143
|
+
backup: true
|
|
9144
|
+
});
|
|
9145
|
+
logger.info(
|
|
9146
|
+
`Updated next.config.${extension} with image optimization`
|
|
9147
|
+
);
|
|
9148
|
+
}
|
|
9149
|
+
} else {
|
|
9150
|
+
const configContent = getNextConfigContent(extension);
|
|
9151
|
+
await writer.createFile(nextConfigPath, configContent);
|
|
9152
|
+
files.push({
|
|
9153
|
+
type: "create",
|
|
9154
|
+
path: normalizePath(nextConfigPath),
|
|
9155
|
+
content: configContent,
|
|
9156
|
+
backup: false
|
|
9157
|
+
});
|
|
9158
|
+
logger.info(`Created next.config.${extension} with image optimization`);
|
|
9159
|
+
}
|
|
9160
|
+
return {
|
|
9161
|
+
files,
|
|
9162
|
+
success: true,
|
|
9163
|
+
message: "Next.js image optimization configured successfully"
|
|
9164
|
+
};
|
|
9165
|
+
} catch (error) {
|
|
9166
|
+
logger.error("Failed to configure image optimization:", error);
|
|
9167
|
+
return {
|
|
9168
|
+
files,
|
|
9169
|
+
success: false,
|
|
9170
|
+
message: `Failed to configure image optimization: ${error instanceof Error ? error.message : String(error)}`
|
|
9171
|
+
};
|
|
9172
|
+
}
|
|
9173
|
+
},
|
|
9174
|
+
/**
|
|
9175
|
+
* Rollback de la configuration
|
|
9176
|
+
*/
|
|
9177
|
+
async rollback(_ctx) {
|
|
9178
|
+
const backupManager = new BackupManager();
|
|
9179
|
+
try {
|
|
9180
|
+
await backupManager.restoreAll();
|
|
9181
|
+
logger.info("Image optimization configuration rolled back");
|
|
9182
|
+
} catch (error) {
|
|
9183
|
+
logger.error(
|
|
9184
|
+
"Failed to rollback image optimization configuration:",
|
|
9185
|
+
error
|
|
9186
|
+
);
|
|
9187
|
+
throw error;
|
|
9188
|
+
}
|
|
9189
|
+
}
|
|
9190
|
+
};
|
|
9191
|
+
function getNextConfigContent(extension) {
|
|
9192
|
+
if (extension === "ts") {
|
|
9193
|
+
return `import type { NextConfig } from 'next'
|
|
9194
|
+
|
|
9195
|
+
const nextConfig: NextConfig = {
|
|
9196
|
+
images: {
|
|
9197
|
+
remotePatterns: [
|
|
9198
|
+
{
|
|
9199
|
+
protocol: 'https',
|
|
9200
|
+
hostname: '**',
|
|
9201
|
+
},
|
|
9202
|
+
],
|
|
9203
|
+
},
|
|
9204
|
+
}
|
|
9205
|
+
|
|
9206
|
+
export default nextConfig
|
|
9207
|
+
`;
|
|
9208
|
+
}
|
|
9209
|
+
return `/** @type {import('next').NextConfig} */
|
|
9210
|
+
const nextConfig = {
|
|
9211
|
+
images: {
|
|
9212
|
+
remotePatterns: [
|
|
9213
|
+
{
|
|
9214
|
+
protocol: 'https',
|
|
9215
|
+
hostname: '**',
|
|
9216
|
+
},
|
|
9217
|
+
],
|
|
9218
|
+
},
|
|
9219
|
+
}
|
|
9220
|
+
|
|
9221
|
+
module.exports = nextConfig
|
|
9222
|
+
`;
|
|
9223
|
+
}
|
|
9224
|
+
function injectImageConfig(content, _extension) {
|
|
9225
|
+
if (content.includes("images:") && content.includes("remotePatterns")) {
|
|
9226
|
+
logger.warn("Image configuration already exists in next.config");
|
|
9227
|
+
return content;
|
|
9228
|
+
}
|
|
9229
|
+
let modifiedContent = content;
|
|
9230
|
+
const configRegex = /(const\s+nextConfig\s*=\s*\{|nextConfig\s*=\s*\{)([\s\S]*?)(\})/m;
|
|
9231
|
+
if (configRegex.test(modifiedContent)) {
|
|
9232
|
+
modifiedContent = modifiedContent.replace(
|
|
9233
|
+
configRegex,
|
|
9234
|
+
(match, start, configContent) => {
|
|
9235
|
+
if (configContent.includes("images:")) {
|
|
9236
|
+
return match;
|
|
9237
|
+
}
|
|
9238
|
+
const trimmed = configContent.trim();
|
|
9239
|
+
const hasTrailingComma = trimmed.endsWith(",");
|
|
9240
|
+
const imageConfig = ` images: {
|
|
9241
|
+
remotePatterns: [
|
|
9242
|
+
{
|
|
9243
|
+
protocol: 'https',
|
|
9244
|
+
hostname: '**',
|
|
9245
|
+
},
|
|
9246
|
+
],
|
|
9247
|
+
},${hasTrailingComma ? "" : "\n"}`;
|
|
9248
|
+
return `${start}${trimmed}${hasTrailingComma ? "" : ","}
|
|
9249
|
+
${imageConfig}
|
|
9250
|
+
}`;
|
|
9251
|
+
}
|
|
9252
|
+
);
|
|
9253
|
+
} else {
|
|
9254
|
+
const imageConfig = `
|
|
9255
|
+
images: {
|
|
9256
|
+
remotePatterns: [
|
|
9257
|
+
{
|
|
9258
|
+
protocol: 'https',
|
|
9259
|
+
hostname: '**',
|
|
9260
|
+
},
|
|
9261
|
+
],
|
|
9262
|
+
},`;
|
|
9263
|
+
modifiedContent = modifiedContent.replace(
|
|
9264
|
+
/(\}\s*)$/,
|
|
9265
|
+
(match) => `${imageConfig}
|
|
9266
|
+
${match}`
|
|
9267
|
+
);
|
|
9268
|
+
}
|
|
9269
|
+
return modifiedContent;
|
|
9270
|
+
}
|
|
9271
|
+
|
|
9272
|
+
// src/plugins/nextjs/font-optimization.ts
|
|
9273
|
+
import { join as join28 } from "path";
|
|
9274
|
+
var nextjsFontOptimizationPlugin = {
|
|
9275
|
+
name: "nextjs-font-optimization",
|
|
9276
|
+
displayName: "Next.js Font Optimization",
|
|
9277
|
+
description: "Configuration de l'optimisation de polices avec next/font",
|
|
9278
|
+
category: "tooling" /* TOOLING */,
|
|
9279
|
+
version: void 0,
|
|
9280
|
+
frameworks: ["nextjs"],
|
|
9281
|
+
/**
|
|
9282
|
+
* Détecte si l'optimisation de polices est déjà configurée
|
|
9283
|
+
*/
|
|
9284
|
+
detect: (ctx) => {
|
|
9285
|
+
return ctx.dependencies["next"] !== void 0 || ctx.devDependencies["next"] !== void 0;
|
|
9286
|
+
},
|
|
9287
|
+
/**
|
|
9288
|
+
* Pas d'installation nécessaire, next/font est inclus dans Next.js
|
|
9289
|
+
*/
|
|
9290
|
+
install(_ctx) {
|
|
9291
|
+
logger.info(
|
|
9292
|
+
"Font optimization is built into Next.js, no installation needed"
|
|
9293
|
+
);
|
|
9294
|
+
return Promise.resolve({
|
|
9295
|
+
packages: {},
|
|
9296
|
+
success: true,
|
|
9297
|
+
message: "Font optimization is built into Next.js"
|
|
9298
|
+
});
|
|
9299
|
+
},
|
|
9300
|
+
/**
|
|
9301
|
+
* Configure l'optimisation de polices dans app/layout.tsx ou pages/_app.tsx
|
|
9302
|
+
*/
|
|
9303
|
+
async configure(ctx) {
|
|
9304
|
+
const backupManager = new BackupManager();
|
|
9305
|
+
const writer = new ConfigWriter(backupManager);
|
|
9306
|
+
const files = [];
|
|
9307
|
+
const projectRoot = ctx.projectRoot;
|
|
9308
|
+
const extension = ctx.typescript ? "tsx" : "jsx";
|
|
9309
|
+
const hasSrcDir = ctx.srcDir && ctx.srcDir !== ".";
|
|
9310
|
+
try {
|
|
9311
|
+
const isAppRouter = ctx.nextjsRouter === "app" || ctx.nextjsRouter === void 0 && await checkPathExists(
|
|
9312
|
+
hasSrcDir ? join28(projectRoot, ctx.srcDir, "app") : join28(projectRoot, "app")
|
|
9313
|
+
);
|
|
9314
|
+
const appLayoutPath = hasSrcDir ? join28(projectRoot, ctx.srcDir, "app", `layout.${extension}`) : join28(projectRoot, "app", `layout.${extension}`);
|
|
9315
|
+
const pagesAppPath = hasSrcDir ? join28(projectRoot, ctx.srcDir, "pages", `_app.${extension}`) : join28(projectRoot, "pages", `_app.${extension}`);
|
|
9316
|
+
let targetPath = null;
|
|
9317
|
+
let targetContent = "";
|
|
9318
|
+
if (isAppRouter) {
|
|
9319
|
+
const appLayoutExists = await checkPathExists(appLayoutPath);
|
|
9320
|
+
if (appLayoutExists) {
|
|
9321
|
+
targetPath = appLayoutPath;
|
|
9322
|
+
targetContent = await readFileContent(appLayoutPath);
|
|
9323
|
+
}
|
|
9324
|
+
} else {
|
|
9325
|
+
const pagesAppExists = await checkPathExists(pagesAppPath);
|
|
9326
|
+
if (pagesAppExists) {
|
|
9327
|
+
targetPath = pagesAppPath;
|
|
9328
|
+
targetContent = await readFileContent(pagesAppPath);
|
|
9329
|
+
}
|
|
9330
|
+
}
|
|
9331
|
+
if (targetPath && targetContent) {
|
|
9332
|
+
const hasFontImport = targetContent.includes("from 'next/font");
|
|
9333
|
+
const hasGoogleFont = targetContent.includes("Inter") || targetContent.includes("Roboto");
|
|
9334
|
+
if (!hasFontImport || !hasGoogleFont) {
|
|
9335
|
+
const updatedContent = injectFontOptimization(
|
|
9336
|
+
targetContent,
|
|
9337
|
+
ctx.typescript,
|
|
9338
|
+
isAppRouter
|
|
9339
|
+
);
|
|
9340
|
+
await writer.writeFile(targetPath, updatedContent, { backup: true });
|
|
9341
|
+
files.push({
|
|
9342
|
+
type: "modify",
|
|
9343
|
+
path: normalizePath(targetPath),
|
|
9344
|
+
content: updatedContent,
|
|
9345
|
+
backup: true
|
|
9346
|
+
});
|
|
9347
|
+
logger.info(`Added font optimization to ${targetPath}`);
|
|
9348
|
+
} else {
|
|
9349
|
+
logger.warn("Font optimization already configured");
|
|
9350
|
+
}
|
|
9351
|
+
} else {
|
|
9352
|
+
logger.warn("Could not find layout or _app file to configure fonts");
|
|
9353
|
+
}
|
|
9354
|
+
return {
|
|
9355
|
+
files,
|
|
9356
|
+
success: true,
|
|
9357
|
+
message: "Next.js font optimization configured successfully"
|
|
9358
|
+
};
|
|
9359
|
+
} catch (error) {
|
|
9360
|
+
logger.error("Failed to configure font optimization:", error);
|
|
9361
|
+
return {
|
|
9362
|
+
files,
|
|
9363
|
+
success: false,
|
|
9364
|
+
message: `Failed to configure font optimization: ${error instanceof Error ? error.message : String(error)}`
|
|
9365
|
+
};
|
|
9366
|
+
}
|
|
9367
|
+
},
|
|
9368
|
+
/**
|
|
9369
|
+
* Rollback de la configuration
|
|
9370
|
+
*/
|
|
9371
|
+
async rollback(_ctx) {
|
|
9372
|
+
const backupManager = new BackupManager();
|
|
9373
|
+
try {
|
|
9374
|
+
await backupManager.restoreAll();
|
|
9375
|
+
logger.info("Font optimization configuration rolled back");
|
|
9376
|
+
} catch (error) {
|
|
9377
|
+
logger.error("Failed to rollback font optimization configuration:", error);
|
|
9378
|
+
throw error;
|
|
9379
|
+
}
|
|
9380
|
+
}
|
|
9381
|
+
};
|
|
9382
|
+
function injectFontOptimization(content, _isTypeScript, isAppRouter) {
|
|
9383
|
+
if (content.includes("from 'next/font") || content.includes('from "next/font')) {
|
|
9384
|
+
return content;
|
|
9385
|
+
}
|
|
9386
|
+
let modifiedContent = content;
|
|
9387
|
+
const fontImport = "import { Inter } from 'next/font/google'\n";
|
|
9388
|
+
const lines = modifiedContent.split("\n");
|
|
9389
|
+
let lastImportIndex = -1;
|
|
9390
|
+
for (let i = 0; i < lines.length; i++) {
|
|
9391
|
+
if (lines[i]?.startsWith("import ")) {
|
|
9392
|
+
lastImportIndex = i;
|
|
9393
|
+
}
|
|
9394
|
+
}
|
|
9395
|
+
if (lastImportIndex >= 0) {
|
|
9396
|
+
lines.splice(lastImportIndex + 1, 0, fontImport.trim());
|
|
9397
|
+
modifiedContent = lines.join("\n");
|
|
9398
|
+
} else {
|
|
9399
|
+
modifiedContent = fontImport + modifiedContent;
|
|
9400
|
+
}
|
|
9401
|
+
const fontConfig = isAppRouter ? `
|
|
9402
|
+
const inter = Inter({ subsets: ['latin'] })
|
|
9403
|
+
` : `
|
|
9404
|
+
const inter = Inter({ subsets: ['latin'] })
|
|
9405
|
+
`;
|
|
9406
|
+
modifiedContent = modifiedContent.replace(
|
|
9407
|
+
/(import[^;]+;[\s\S]*?)(\n)/,
|
|
9408
|
+
(match) => `${match}${fontConfig}`
|
|
9409
|
+
);
|
|
9410
|
+
if (isAppRouter) {
|
|
9411
|
+
if (modifiedContent.includes("<body")) {
|
|
9412
|
+
modifiedContent = modifiedContent.replace(
|
|
9413
|
+
/<body([^>]*)>/,
|
|
9414
|
+
(match, attrs) => {
|
|
9415
|
+
if (attrs.includes("className")) {
|
|
9416
|
+
return match;
|
|
9417
|
+
}
|
|
9418
|
+
return `<body${attrs} className={inter.className}>`;
|
|
9419
|
+
}
|
|
9420
|
+
);
|
|
9421
|
+
}
|
|
9422
|
+
} else {
|
|
9423
|
+
if (modifiedContent.includes("return (")) {
|
|
9424
|
+
modifiedContent = modifiedContent.replace(
|
|
9425
|
+
/(return\s*\([\s\S]*?<div[^>]*)/,
|
|
9426
|
+
(match) => {
|
|
9427
|
+
if (match.includes("className")) {
|
|
9428
|
+
return match;
|
|
9429
|
+
}
|
|
9430
|
+
return `${match} className={inter.className}`;
|
|
9431
|
+
}
|
|
9432
|
+
);
|
|
9433
|
+
}
|
|
9434
|
+
}
|
|
9435
|
+
return modifiedContent;
|
|
9436
|
+
}
|
|
9437
|
+
|
|
9438
|
+
// src/plugins/nextjs/middleware.ts
|
|
9439
|
+
import { join as join29 } from "path";
|
|
9440
|
+
var nextjsMiddlewarePlugin = {
|
|
9441
|
+
name: "nextjs-middleware",
|
|
9442
|
+
displayName: "Next.js Middleware",
|
|
9443
|
+
description: "Template de middleware pour Next.js",
|
|
9444
|
+
category: "tooling" /* TOOLING */,
|
|
9445
|
+
version: void 0,
|
|
9446
|
+
frameworks: ["nextjs"],
|
|
9447
|
+
/**
|
|
9448
|
+
* Détecte si le middleware existe déjà
|
|
9449
|
+
*/
|
|
9450
|
+
detect: (_ctx) => {
|
|
9451
|
+
return false;
|
|
9452
|
+
},
|
|
9453
|
+
/**
|
|
9454
|
+
* Pas d'installation nécessaire
|
|
9455
|
+
*/
|
|
9456
|
+
install(_ctx) {
|
|
9457
|
+
logger.info("Middleware is a file, no installation needed");
|
|
9458
|
+
return Promise.resolve({
|
|
9459
|
+
packages: {},
|
|
9460
|
+
success: true,
|
|
9461
|
+
message: "Middleware is a file, no installation needed"
|
|
9462
|
+
});
|
|
9463
|
+
},
|
|
9464
|
+
/**
|
|
9465
|
+
* Crée le fichier middleware.ts/js à la racine du projet
|
|
9466
|
+
*/
|
|
9467
|
+
async configure(ctx) {
|
|
9468
|
+
const backupManager = new BackupManager();
|
|
9469
|
+
const writer = new ConfigWriter(backupManager);
|
|
9470
|
+
const files = [];
|
|
9471
|
+
const projectRoot = ctx.projectRoot;
|
|
9472
|
+
const extension = ctx.typescript ? "ts" : "js";
|
|
9473
|
+
try {
|
|
9474
|
+
const middlewarePath = join29(projectRoot, `middleware.${extension}`);
|
|
9475
|
+
const middlewareExists = await checkPathExists(middlewarePath);
|
|
9476
|
+
if (middlewareExists) {
|
|
9477
|
+
logger.warn("middleware.ts already exists, skipping creation");
|
|
9478
|
+
} else {
|
|
9479
|
+
const middlewareContent = getMiddlewareContent(extension);
|
|
9480
|
+
await writer.createFile(middlewarePath, middlewareContent);
|
|
9481
|
+
files.push({
|
|
9482
|
+
type: "create",
|
|
9483
|
+
path: normalizePath(middlewarePath),
|
|
9484
|
+
content: middlewareContent,
|
|
9485
|
+
backup: false
|
|
9486
|
+
});
|
|
9487
|
+
logger.info(`Created middleware.${extension}`);
|
|
9488
|
+
}
|
|
9489
|
+
return {
|
|
9490
|
+
files,
|
|
9491
|
+
success: true,
|
|
9492
|
+
message: "Next.js middleware created successfully"
|
|
9493
|
+
};
|
|
9494
|
+
} catch (error) {
|
|
9495
|
+
logger.error("Failed to create middleware:", error);
|
|
9496
|
+
return {
|
|
9497
|
+
files,
|
|
9498
|
+
success: false,
|
|
9499
|
+
message: `Failed to create middleware: ${error instanceof Error ? error.message : String(error)}`
|
|
9500
|
+
};
|
|
9501
|
+
}
|
|
9502
|
+
},
|
|
9503
|
+
/**
|
|
9504
|
+
* Rollback de la configuration
|
|
9505
|
+
*/
|
|
9506
|
+
async rollback(_ctx) {
|
|
9507
|
+
const backupManager = new BackupManager();
|
|
9508
|
+
try {
|
|
9509
|
+
await backupManager.restoreAll();
|
|
9510
|
+
logger.info("Middleware configuration rolled back");
|
|
9511
|
+
} catch (error) {
|
|
9512
|
+
logger.error("Failed to rollback middleware configuration:", error);
|
|
9513
|
+
throw error;
|
|
9514
|
+
}
|
|
9515
|
+
}
|
|
9516
|
+
};
|
|
9517
|
+
function getMiddlewareContent(extension) {
|
|
9518
|
+
if (extension === "ts") {
|
|
9519
|
+
return `import { NextResponse } from 'next/server'
|
|
9520
|
+
import type { NextRequest } from 'next/server'
|
|
9521
|
+
|
|
9522
|
+
export function middleware(request: NextRequest) {
|
|
9523
|
+
// Ajoutez votre logique de middleware ici
|
|
9524
|
+
// Exemple : redirection, authentification, etc.
|
|
9525
|
+
|
|
9526
|
+
return NextResponse.next()
|
|
9527
|
+
}
|
|
9528
|
+
|
|
9529
|
+
// Configurez les chemins sur lesquels le middleware s'applique
|
|
9530
|
+
export const config = {
|
|
9531
|
+
matcher: [
|
|
9532
|
+
/*
|
|
9533
|
+
* Match all request paths except for the ones starting with:
|
|
9534
|
+
* - api (API routes)
|
|
9535
|
+
* - _next/static (static files)
|
|
9536
|
+
* - _next/image (image optimization files)
|
|
9537
|
+
* - favicon.ico (favicon file)
|
|
9538
|
+
*/
|
|
9539
|
+
'/((?!api|_next/static|_next/image|favicon.ico).*)',
|
|
9540
|
+
],
|
|
9541
|
+
}
|
|
9542
|
+
`;
|
|
9543
|
+
}
|
|
9544
|
+
return `import { NextResponse } from 'next/server'
|
|
9545
|
+
|
|
9546
|
+
export function middleware(request) {
|
|
9547
|
+
// Ajoutez votre logique de middleware ici
|
|
9548
|
+
// Exemple : redirection, authentification, etc.
|
|
9549
|
+
|
|
9550
|
+
return NextResponse.next()
|
|
9551
|
+
}
|
|
9552
|
+
|
|
9553
|
+
// Configurez les chemins sur lesquels le middleware s'applique
|
|
9554
|
+
export const config = {
|
|
9555
|
+
matcher: [
|
|
9556
|
+
/*
|
|
9557
|
+
* Match all request paths except for the ones starting with:
|
|
9558
|
+
* - api (API routes)
|
|
9559
|
+
* - _next/static (static files)
|
|
9560
|
+
* - _next/image (image optimization files)
|
|
9561
|
+
* - favicon.ico (favicon file)
|
|
9562
|
+
*/
|
|
9563
|
+
'/((?!api|_next/static|_next/image|favicon.ico).*)',
|
|
9564
|
+
],
|
|
9565
|
+
}
|
|
9566
|
+
`;
|
|
9567
|
+
}
|
|
9568
|
+
|
|
9569
|
+
// src/plugins/nextjs/api-routes.ts
|
|
9570
|
+
import { join as join30 } from "path";
|
|
9571
|
+
var nextjsApiRoutesPlugin = {
|
|
9572
|
+
name: "nextjs-api-routes",
|
|
9573
|
+
displayName: "Next.js API Routes",
|
|
9574
|
+
description: "Template d'API routes pour Next.js",
|
|
9575
|
+
category: "tooling" /* TOOLING */,
|
|
9576
|
+
version: void 0,
|
|
9577
|
+
frameworks: ["nextjs"],
|
|
9578
|
+
/**
|
|
9579
|
+
* Détecte si des API routes existent déjà
|
|
9580
|
+
*/
|
|
9581
|
+
detect: (_ctx) => {
|
|
9582
|
+
return false;
|
|
9583
|
+
},
|
|
9584
|
+
/**
|
|
9585
|
+
* Pas d'installation nécessaire
|
|
9586
|
+
*/
|
|
9587
|
+
install(_ctx) {
|
|
9588
|
+
logger.info("API routes are files, no installation needed");
|
|
9589
|
+
return Promise.resolve({
|
|
9590
|
+
packages: {},
|
|
9591
|
+
success: true,
|
|
9592
|
+
message: "API routes are files, no installation needed"
|
|
9593
|
+
});
|
|
9594
|
+
},
|
|
9595
|
+
/**
|
|
9596
|
+
* Crée un exemple d'API route
|
|
9597
|
+
*/
|
|
9598
|
+
async configure(ctx) {
|
|
9599
|
+
const backupManager = new BackupManager();
|
|
9600
|
+
const writer = new ConfigWriter(backupManager);
|
|
9601
|
+
const files = [];
|
|
9602
|
+
const projectRoot = ctx.projectRoot;
|
|
9603
|
+
const extension = ctx.typescript ? "ts" : "js";
|
|
9604
|
+
const hasSrcDir = ctx.srcDir && ctx.srcDir !== ".";
|
|
9605
|
+
try {
|
|
9606
|
+
const appApiPath = hasSrcDir ? join30(
|
|
9607
|
+
projectRoot,
|
|
9608
|
+
ctx.srcDir,
|
|
9609
|
+
"app",
|
|
9610
|
+
"api",
|
|
9611
|
+
"hello",
|
|
9612
|
+
`route.${extension}`
|
|
9613
|
+
) : join30(projectRoot, "app", "api", "hello", `route.${extension}`);
|
|
9614
|
+
const pagesApiPath = hasSrcDir ? join30(projectRoot, ctx.srcDir, "pages", "api", `hello.${extension}`) : join30(projectRoot, "pages", "api", `hello.${extension}`);
|
|
9615
|
+
const appApiExists = await checkPathExists(appApiPath);
|
|
9616
|
+
const pagesApiExists = await checkPathExists(pagesApiPath);
|
|
9617
|
+
if (appApiExists || pagesApiExists) {
|
|
9618
|
+
logger.warn("API route already exists");
|
|
9619
|
+
return {
|
|
9620
|
+
files: [],
|
|
9621
|
+
success: true,
|
|
9622
|
+
message: "API route already exists"
|
|
9623
|
+
};
|
|
9624
|
+
}
|
|
9625
|
+
const isAppRouter = ctx.nextjsRouter === "app" || ctx.nextjsRouter === void 0 && await checkPathExists(
|
|
9626
|
+
hasSrcDir ? join30(projectRoot, ctx.srcDir, "app") : join30(projectRoot, "app")
|
|
9627
|
+
);
|
|
9628
|
+
let targetPath;
|
|
9629
|
+
if (isAppRouter) {
|
|
9630
|
+
targetPath = appApiPath;
|
|
9631
|
+
await ensureDirectory(
|
|
9632
|
+
join30(projectRoot, hasSrcDir ? ctx.srcDir : ".", "app", "api", "hello")
|
|
9633
|
+
);
|
|
9634
|
+
} else {
|
|
9635
|
+
targetPath = pagesApiPath;
|
|
9636
|
+
await ensureDirectory(
|
|
9637
|
+
join30(projectRoot, hasSrcDir ? ctx.srcDir : ".", "pages", "api")
|
|
9638
|
+
);
|
|
9639
|
+
}
|
|
9640
|
+
const apiContent = getApiRouteContent(extension, isAppRouter);
|
|
9641
|
+
await writer.createFile(targetPath, apiContent);
|
|
9642
|
+
files.push({
|
|
9643
|
+
type: "create",
|
|
9644
|
+
path: normalizePath(targetPath),
|
|
9645
|
+
content: apiContent,
|
|
9646
|
+
backup: false
|
|
9647
|
+
});
|
|
9648
|
+
logger.info(`Created API route: ${targetPath}`);
|
|
9649
|
+
return {
|
|
9650
|
+
files,
|
|
9651
|
+
success: true,
|
|
9652
|
+
message: "Next.js API route created successfully"
|
|
9653
|
+
};
|
|
9654
|
+
} catch (error) {
|
|
9655
|
+
logger.error("Failed to create API route:", error);
|
|
9656
|
+
return {
|
|
9657
|
+
files,
|
|
9658
|
+
success: false,
|
|
9659
|
+
message: `Failed to create API route: ${error instanceof Error ? error.message : String(error)}`
|
|
9660
|
+
};
|
|
9661
|
+
}
|
|
9662
|
+
},
|
|
9663
|
+
/**
|
|
9664
|
+
* Rollback de la configuration
|
|
9665
|
+
*/
|
|
9666
|
+
async rollback(_ctx) {
|
|
9667
|
+
const backupManager = new BackupManager();
|
|
9668
|
+
try {
|
|
9669
|
+
await backupManager.restoreAll();
|
|
9670
|
+
logger.info("API route configuration rolled back");
|
|
9671
|
+
} catch (error) {
|
|
9672
|
+
logger.error("Failed to rollback API route configuration:", error);
|
|
9673
|
+
throw error;
|
|
9674
|
+
}
|
|
9675
|
+
}
|
|
9676
|
+
};
|
|
9677
|
+
function getApiRouteContent(extension, isAppRouter) {
|
|
9678
|
+
if (isAppRouter) {
|
|
9679
|
+
if (extension === "ts") {
|
|
9680
|
+
return `import { NextResponse } from 'next/server'
|
|
9681
|
+
import type { NextRequest } from 'next/server'
|
|
9682
|
+
|
|
9683
|
+
export async function GET(request: NextRequest) {
|
|
9684
|
+
return NextResponse.json({ message: 'Hello from Next.js API!' })
|
|
9685
|
+
}
|
|
9686
|
+
|
|
9687
|
+
export async function POST(request: NextRequest) {
|
|
9688
|
+
const body = await request.json()
|
|
9689
|
+
return NextResponse.json({ message: 'POST request received', data: body })
|
|
9690
|
+
}
|
|
9691
|
+
`;
|
|
9692
|
+
}
|
|
9693
|
+
return `import { NextResponse } from 'next/server'
|
|
9694
|
+
|
|
9695
|
+
export async function GET(request) {
|
|
9696
|
+
return NextResponse.json({ message: 'Hello from Next.js API!' })
|
|
9697
|
+
}
|
|
9698
|
+
|
|
9699
|
+
export async function POST(request) {
|
|
9700
|
+
const body = await request.json()
|
|
9701
|
+
return NextResponse.json({ message: 'POST request received', data: body })
|
|
9702
|
+
}
|
|
9703
|
+
`;
|
|
9704
|
+
} else {
|
|
9705
|
+
if (extension === "ts") {
|
|
9706
|
+
return `import type { NextApiRequest, NextApiResponse } from 'next'
|
|
9707
|
+
|
|
9708
|
+
type Data = {
|
|
9709
|
+
message: string
|
|
9710
|
+
}
|
|
9711
|
+
|
|
9712
|
+
export default function handler(
|
|
9713
|
+
req: NextApiRequest,
|
|
9714
|
+
res: NextApiResponse<Data>
|
|
9715
|
+
) {
|
|
9716
|
+
if (req.method === 'GET') {
|
|
9717
|
+
res.status(200).json({ message: 'Hello from Next.js API!' })
|
|
9718
|
+
} else if (req.method === 'POST') {
|
|
9719
|
+
res.status(200).json({ message: 'POST request received', data: req.body })
|
|
9720
|
+
} else {
|
|
9721
|
+
res.setHeader('Allow', ['GET', 'POST'])
|
|
9722
|
+
res.status(405).end(\`Method \${req.method} Not Allowed\`)
|
|
9723
|
+
}
|
|
9724
|
+
}
|
|
9725
|
+
`;
|
|
9726
|
+
}
|
|
9727
|
+
return `export default function handler(req, res) {
|
|
9728
|
+
if (req.method === 'GET') {
|
|
9729
|
+
res.status(200).json({ message: 'Hello from Next.js API!' })
|
|
9730
|
+
} else if (req.method === 'POST') {
|
|
9731
|
+
res.status(200).json({ message: 'POST request received', data: req.body })
|
|
9732
|
+
} else {
|
|
9733
|
+
res.setHeader('Allow', ['GET', 'POST'])
|
|
9734
|
+
res.status(405).end(\`Method \${req.method} Not Allowed\`)
|
|
9735
|
+
}
|
|
9736
|
+
}
|
|
9737
|
+
`;
|
|
9738
|
+
}
|
|
9739
|
+
}
|
|
9740
|
+
|
|
8086
9741
|
// src/plugins/registry.ts
|
|
8087
9742
|
var pluginRegistry = [
|
|
8088
9743
|
// Routing
|
|
@@ -8097,6 +9752,7 @@ var pluginRegistry = [
|
|
|
8097
9752
|
tanstackQueryPlugin,
|
|
8098
9753
|
// CSS
|
|
8099
9754
|
tailwindcssPlugin,
|
|
9755
|
+
tailwindcssNextjsPlugin,
|
|
8100
9756
|
styledComponentsPlugin,
|
|
8101
9757
|
emotionPlugin,
|
|
8102
9758
|
reactBootstrapPlugin,
|
|
@@ -8105,9 +9761,11 @@ var pluginRegistry = [
|
|
|
8105
9761
|
zodPlugin,
|
|
8106
9762
|
// UI
|
|
8107
9763
|
shadcnUiPlugin,
|
|
9764
|
+
shadcnUiNextjsPlugin,
|
|
8108
9765
|
radixUiPlugin,
|
|
8109
9766
|
reactIconsPlugin,
|
|
8110
9767
|
reactHotToastPlugin,
|
|
9768
|
+
reactHotToastNextjsPlugin,
|
|
8111
9769
|
framerMotionPlugin,
|
|
8112
9770
|
// Tooling
|
|
8113
9771
|
eslintPlugin,
|
|
@@ -8115,7 +9773,12 @@ var pluginRegistry = [
|
|
|
8115
9773
|
huskyPlugin,
|
|
8116
9774
|
dateFnsPlugin,
|
|
8117
9775
|
// Testing
|
|
8118
|
-
reactTestingLibraryPlugin
|
|
9776
|
+
reactTestingLibraryPlugin,
|
|
9777
|
+
// Next.js specific
|
|
9778
|
+
nextjsImageOptimizationPlugin,
|
|
9779
|
+
nextjsFontOptimizationPlugin,
|
|
9780
|
+
nextjsMiddlewarePlugin,
|
|
9781
|
+
nextjsApiRoutesPlugin
|
|
8119
9782
|
// etc.
|
|
8120
9783
|
];
|
|
8121
9784
|
function validatePlugin(plugin) {
|