@fraza/start 0.0.0-stage → 0.3.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/CHANGELOG.md +180 -0
- package/LICENSE +21 -0
- package/README.md +169 -2
- package/THIRD-PARTY-NOTICES.md +11 -0
- package/bin/agents-block.md +61 -0
- package/bin/cli.js +197 -0
- package/bin/project.js +281 -0
- package/bin/version.js +2 -0
- package/changelog.json +577 -0
- package/package.json +42 -4
- package/templates/app/(admin)/admin/[...admin]/layout.js +3 -0
- package/templates/app/(admin)/admin/[...admin]/loading.js +3 -0
- package/templates/app/(admin)/admin/[...admin]/page.js +1 -0
- package/templates/app/(admin)/admin/error.js +4 -0
- package/templates/app/(admin)/admin/layout.js +17 -0
- package/templates/app/(admin)/admin/not-found.js +2 -0
- package/templates/app/(admin)/admin/page.js +5 -0
- package/templates/app/(auth)/auth/[...auth]/page.js +1 -0
- package/templates/app/(auth)/auth/page.js +5 -0
- package/templates/app/(public)/[...slug]/page.js +29 -0
- package/templates/app/(public)/error.js +6 -0
- package/templates/app/(public)/layout.js +17 -0
- package/templates/app/(public)/not-found.js +20 -0
- package/templates/app/(public)/page.js +248 -0
- package/templates/app/.fraza/blocks.client.js +8 -0
- package/templates/app/.fraza/blocks.server.js +4 -0
- package/templates/app/.fraza/modules.client.js +6 -0
- package/templates/app/.fraza/modules.generated.js +3 -0
- package/templates/app/.fraza/public-pages.server.js +4 -0
- package/templates/app/.fraza/sources.css +2 -0
- package/templates/app/.fraza/templates.server.js +5 -0
- package/templates/app/global-error.js +5 -0
- package/templates/app/icon.svg +1 -0
- package/templates/app/layout.js +31 -0
- package/templates/app/za/[...path]/not-found.js +6 -0
- package/templates/app/za/[...path]/page.js +8 -0
- package/templates/app/za/api/[...path]/route.js +1 -0
- package/templates/app/za/media/[filename]/route.js +1 -0
- package/templates/components/fraza/blocks/README.md +207 -0
- package/templates/components/fraza/blocks/accordion/fields.js +19 -0
- package/templates/components/fraza/blocks/accordion/render.js +47 -0
- package/templates/components/fraza/blocks/accordion/template.js +28 -0
- package/templates/components/fraza/blocks/callout/fields.js +15 -0
- package/templates/components/fraza/blocks/callout/render.js +28 -0
- package/templates/components/fraza/blocks/callout/template.js +37 -0
- package/templates/components/fraza/blocks/icon-list/fields.js +20 -0
- package/templates/components/fraza/blocks/icon-list/render.js +44 -0
- package/templates/components/fraza/blocks/icon-list/template.js +89 -0
- package/templates/components/fraza/blocks/three-cards/fields.js +19 -0
- package/templates/components/fraza/blocks/three-cards/render.js +33 -0
- package/templates/components/fraza/blocks/three-cards/template.js +57 -0
- package/templates/components/fraza/templates/README.md +293 -0
- package/templates/components/fraza/templates/content.js +271 -0
- package/templates/components/fraza/templates/default.js +18 -0
- package/templates/components/fraza/templates/post-blogue.js +97 -0
- package/templates/instrumentation.js +21 -0
- package/templates/jsconfig.json +9 -0
- package/templates/styles/globals.css +25 -0
- package/templates/theme.js +25 -0
package/bin/project.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// Étapes de génération d'un projet Fraza, sans effet de bord à l'import.
|
|
2
|
+
//
|
|
3
|
+
// `cli.js` les enchaîne ; les tests de `test/` les appellent une par une sur un
|
|
4
|
+
// dossier temporaire. Chaque fonction reçoit les chemins qu'elle touche : aucune
|
|
5
|
+
// ne lit le dossier courant d'elle-même.
|
|
6
|
+
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
export const REGISTRY_HOST = 'registry.fraza.ca';
|
|
12
|
+
export const REGISTRY_URL = `https://${REGISTRY_HOST}/`;
|
|
13
|
+
|
|
14
|
+
/** Plancher de Node, le même que `engines.node` et que le core. */
|
|
15
|
+
export const MIN_NODE = [20, 12];
|
|
16
|
+
|
|
17
|
+
// Version de `create-next-app`, donc de Next, installée dans le projet généré.
|
|
18
|
+
// C'est le dernier correctif de la version mineure contre laquelle le core a
|
|
19
|
+
// été validé (`packages.next.tested` de son `supported.json`) : un correctif de
|
|
20
|
+
// Next ne change pas d'API, et il porte souvent une faille corrigée : 16.3.5,
|
|
21
|
+
// validée d'abord, est touchée par GHSA-vcvr-r3jv-pc5j, corrigée en 16.3.8,
|
|
22
|
+
// la version que le core déclare maintenant.
|
|
23
|
+
// `npm run check:contract` échoue si l'épingle quitte la version mineure
|
|
24
|
+
// validée ou passe sous la version validée (`pinFollowsTestedNext`).
|
|
25
|
+
export const CREATE_NEXT_APP_VERSION = '16.3.8';
|
|
26
|
+
|
|
27
|
+
// Entrées tolérées dans le dossier cible : métadonnées d'éditeur ou de gestion
|
|
28
|
+
// de versions, que ni `create-next-app` ni ce CLI n'écrivent. Toute autre entrée
|
|
29
|
+
// ferait écraser un fichier existant, ou échouer `create-next-app` après coup.
|
|
30
|
+
const ALLOWED_ENTRIES = new Set([
|
|
31
|
+
'.claude',
|
|
32
|
+
'.cursor',
|
|
33
|
+
'.DS_Store',
|
|
34
|
+
'.git',
|
|
35
|
+
'.gitattributes',
|
|
36
|
+
'.hg',
|
|
37
|
+
'.idea',
|
|
38
|
+
'.vscode',
|
|
39
|
+
'.zed',
|
|
40
|
+
'LICENSE',
|
|
41
|
+
'Thumbs.db',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// `create-next-app` écrit un AGENTS.md, et `next dev` le réécrit à chaque
|
|
45
|
+
// démarrage. Il ne remplace que ce qui se trouve entre ses propres marqueurs et
|
|
46
|
+
// laisse le reste du fichier intact : notre bloc, posé à la suite avec ses
|
|
47
|
+
// marqueurs à lui, survit donc aux régénérations. On le délimite pareillement
|
|
48
|
+
// pour pouvoir le mettre à jour sans le dupliquer.
|
|
49
|
+
export const AGENTS_BLOCK_START = '<!-- BEGIN:fraza-agent-rules -->';
|
|
50
|
+
export const AGENTS_BLOCK_END = '<!-- END:fraza-agent-rules -->';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compare la version de Node au plancher.
|
|
54
|
+
* @param {string} version Par exemple `process.versions.node`.
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
export function isSupportedNode(version) {
|
|
58
|
+
const [major, minor] = String(version).split('.').map(Number);
|
|
59
|
+
if (major !== MIN_NODE[0]) return major > MIN_NODE[0];
|
|
60
|
+
return minor >= MIN_NODE[1];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Dit si l'épingle de `create-next-app` suit la version de Next validée par le
|
|
65
|
+
* core : même version majeure et mineure, et pas en dessous.
|
|
66
|
+
* @param {string} pin
|
|
67
|
+
* @param {string} tested
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
export function pinFollowsTestedNext(pin, tested) {
|
|
71
|
+
const [pinMajor, pinMinor, pinPatch] = String(pin).split('.').map(Number);
|
|
72
|
+
const [major, minor, patch] = String(tested).split('.').map(Number);
|
|
73
|
+
return pinMajor === major && pinMinor === minor && pinPatch >= patch;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Liste les entrées du dossier qui empêchent d'y générer un projet.
|
|
78
|
+
* @param {string} dir
|
|
79
|
+
* @returns {string[]}
|
|
80
|
+
*/
|
|
81
|
+
export function findConflictingEntries(dir) {
|
|
82
|
+
return fs.readdirSync(dir).filter((name) => !ALLOWED_ENTRIES.has(name)).sort();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Ajoute, ou remplace, le bloc Fraza dans l'AGENTS.md du projet généré. Un agent
|
|
87
|
+
* qui ouvre le dépôt apprend ainsi d'où vient le code et où lire la doc livrée
|
|
88
|
+
* dans `node_modules/@fraza/core/dist/docs/`.
|
|
89
|
+
* @param {string} agentsPath
|
|
90
|
+
* @param {string} block
|
|
91
|
+
*/
|
|
92
|
+
export function writeAgentsBlock(agentsPath, block) {
|
|
93
|
+
if (!fs.existsSync(agentsPath)) {
|
|
94
|
+
fs.writeFileSync(agentsPath, `${block}\n`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const existing = fs.readFileSync(agentsPath, 'utf-8');
|
|
99
|
+
const start = existing.indexOf(AGENTS_BLOCK_START);
|
|
100
|
+
const end = existing.indexOf(AGENTS_BLOCK_END, start);
|
|
101
|
+
|
|
102
|
+
if (start !== -1 && end !== -1) {
|
|
103
|
+
const before = existing.slice(0, start);
|
|
104
|
+
const after = existing.slice(end + AGENTS_BLOCK_END.length);
|
|
105
|
+
fs.writeFileSync(agentsPath, before + block + after);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const separator = existing.length === 0 || existing.endsWith('\n') ? '\n' : '\n\n';
|
|
110
|
+
fs.writeFileSync(agentsPath, `${existing}${separator}${block}\n`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Masque l'indicateur de développement de Next dans le `next.config.mjs` écrit
|
|
115
|
+
* par `create-next-app`. Le fichier est complété, jamais remplacé : avec
|
|
116
|
+
* Turbopack, `create-next-app` peut y brancher Tailwind (règle `turbopack.rules`
|
|
117
|
+
* vers `@tailwindcss/turbopack`, sans `postcss.config.mjs`). Un gabarit qui
|
|
118
|
+
* l'écraserait laisserait alors le projet sans aucun style.
|
|
119
|
+
* @param {string} configPath
|
|
120
|
+
* @returns {boolean} `false` si le fichier manque ou n'a pas la forme attendue.
|
|
121
|
+
*/
|
|
122
|
+
export function hideDevIndicators(configPath) {
|
|
123
|
+
if (!fs.existsSync(configPath)) return false;
|
|
124
|
+
|
|
125
|
+
const existing = fs.readFileSync(configPath, 'utf-8');
|
|
126
|
+
if (existing.includes('devIndicators')) return true;
|
|
127
|
+
|
|
128
|
+
const pattern = /(const nextConfig = \{\n)/;
|
|
129
|
+
if (!pattern.test(existing)) return false;
|
|
130
|
+
|
|
131
|
+
fs.writeFileSync(configPath, existing.replace(pattern, '$1 devIndicators: false,\n'));
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Complète le `package.json` écrit par `create-next-app` : `fraza-modules sync`
|
|
137
|
+
* avant `next dev` et `next build`, et à chaque installation.
|
|
138
|
+
* @param {object} pkg
|
|
139
|
+
* @param {Record<string, string>} siteOverrides
|
|
140
|
+
* @returns {object} Un nouvel objet, `pkg` n'est pas modifié.
|
|
141
|
+
*/
|
|
142
|
+
export function withFrazaScripts(pkg, siteOverrides = {}) {
|
|
143
|
+
const next = { ...pkg, scripts: { ...pkg.scripts } };
|
|
144
|
+
next.scripts.postinstall = 'fraza-modules sync';
|
|
145
|
+
next.scripts.dev = 'fraza-modules sync && next dev';
|
|
146
|
+
next.scripts.build = 'fraza-modules sync && next build';
|
|
147
|
+
next.scripts.audit = 'npm audit --omit=dev';
|
|
148
|
+
|
|
149
|
+
const overrides = { ...siteOverrides, ...pkg.overrides };
|
|
150
|
+
if (Object.keys(overrides).length > 0) next.overrides = overrides;
|
|
151
|
+
return next;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Ajoute au `.gitignore` de `create-next-app` les deux règles de Fraza.
|
|
156
|
+
*
|
|
157
|
+
* Les règles `.env*` de `create-next-app` restent actives : les valeurs d'une
|
|
158
|
+
* machine (`.env.local`) et les secrets ne sont jamais suivis. Seul le modèle
|
|
159
|
+
* `.env.example`, sans aucune valeur secrète, est rendu à git. Le `.npmrc` n'est
|
|
160
|
+
* pas ignoré : il ne porte que les scopes du registre, dont la machine de build
|
|
161
|
+
* a besoin pour installer `@fraza/core`.
|
|
162
|
+
* @param {string} gitignore
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
export function withFrazaGitignore(gitignore) {
|
|
166
|
+
let next = gitignore === '' || gitignore.endsWith('\n') ? gitignore : `${gitignore}\n`;
|
|
167
|
+
const add = (lines) => {
|
|
168
|
+
next += `\n${lines.join('\n')}\n`;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// La négation n'agit que placée après la règle qui ignore : sans `.env*`
|
|
172
|
+
// plus haut, elle ne protégerait rien.
|
|
173
|
+
if (!/^\.env\*$/m.test(next)) {
|
|
174
|
+
add(['# Variables d\'environnement : jamais suivies par git.', '.env*']);
|
|
175
|
+
}
|
|
176
|
+
if (!/^!\.env\.example$/m.test(next)) {
|
|
177
|
+
add(['# Modèle des variables, sans aucune valeur secrète.', '!.env.example']);
|
|
178
|
+
}
|
|
179
|
+
if (!/^\/?app\/\.fraza\/$/m.test(next)) {
|
|
180
|
+
add(['# Généré par fraza-modules sync.', 'app/.fraza/']);
|
|
181
|
+
}
|
|
182
|
+
return next;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Écrit le `.npmrc` du projet : le scope `@fraza-pro` vers le registre, rien
|
|
187
|
+
* d'autre. `@fraza` n'y figure pas : le core et les modules gratuits viennent
|
|
188
|
+
* de npmjs, le registre par défaut. Aucun jeton n'y est jamais recopié :
|
|
189
|
+
* `npx fraza-key` ajoute le jeton le jour où un module Pro arrive. Sans
|
|
190
|
+
* secret, le fichier reste suivi par git, et la machine de build sait où
|
|
191
|
+
* chercher les modules Pro.
|
|
192
|
+
*
|
|
193
|
+
* Le fichier est écrit ici, et non par `fraza-key` du core, parce qu'il doit
|
|
194
|
+
* exister avant l'installation de `@fraza/core`. Il n'est pas non plus copié
|
|
195
|
+
* depuis les gabarits : npm exclut toujours les `.npmrc` d'un paquet publié.
|
|
196
|
+
* @param {string} npmrcPath
|
|
197
|
+
*/
|
|
198
|
+
export function writeProjectNpmrc(npmrcPath) {
|
|
199
|
+
fs.writeFileSync(npmrcPath, `@fraza-pro:registry=${REGISTRY_URL}\n`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* En-tête de `.env.local`. Celui du modèle décrit `.env.example` : un fichier
|
|
204
|
+
* suivi par git, sans secret, lu par rien. Les trois sont faux pour
|
|
205
|
+
* `.env.local`, le seul fichier du projet qui ne doit jamais être commité.
|
|
206
|
+
*/
|
|
207
|
+
export const LOCAL_ENV_HEADER = [
|
|
208
|
+
'# Valeurs de cette machine, ignorées par git, lues par Next.js et les CLI',
|
|
209
|
+
'# fraza-*. Ne jamais commiter ce fichier : il porte FRAZA_ENCRYPTION_KEY.',
|
|
210
|
+
'#',
|
|
211
|
+
'# En production, ces variables se saisissent dans la plateforme',
|
|
212
|
+
"# d'hébergement. FRAZA_ENCRYPTION_KEY s'y copie telle quelle : elle ne doit",
|
|
213
|
+
'# jamais changer.',
|
|
214
|
+
].join('\n');
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Remplace le bloc de commentaires qui ouvre le modèle par `LOCAL_ENV_HEADER`,
|
|
218
|
+
* ou l'ajoute quand le modèle n'en a pas.
|
|
219
|
+
* @param {string} template
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
function withLocalHeader(template) {
|
|
223
|
+
const lines = template.split('\n');
|
|
224
|
+
let start = 0;
|
|
225
|
+
while (start < lines.length && lines[start].startsWith('#')) start += 1;
|
|
226
|
+
const rest = lines.slice(start).join('\n');
|
|
227
|
+
return start > 0 ? `${LOCAL_ENV_HEADER}\n${rest}` : `${LOCAL_ENV_HEADER}\n\n${rest}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Remplit les valeurs propres à cette machine dans une copie du modèle :
|
|
232
|
+
* `FRAZA_ENCRYPTION_KEY` générée et `NEXT_PUBLIC_URL` locale, sous l'en-tête
|
|
233
|
+
* de `.env.local` (`LOCAL_ENV_HEADER`).
|
|
234
|
+
*
|
|
235
|
+
* La clé n'a rien à saisir à la main : c'est 32 octets aléatoires propres au
|
|
236
|
+
* projet, que `@fraza/core/crypto` attend en 64 caractères hexadécimaux. Une clé
|
|
237
|
+
* déjà présente n'est jamais remplacée : la changer rendrait illisible tout ce
|
|
238
|
+
* qui a été chiffré avec l'ancienne.
|
|
239
|
+
* @param {string} template Contenu du `.env.example` du core.
|
|
240
|
+
* @param {{ key?: string, publicUrl?: string }} [opts]
|
|
241
|
+
* @returns {string}
|
|
242
|
+
*/
|
|
243
|
+
export function fillLocalEnv(template, { key = randomBytes(32).toString('hex'), publicUrl = 'http://localhost:3000' } = {}) {
|
|
244
|
+
const setEmpty = (contents, name, value) => {
|
|
245
|
+
const line = new RegExp(`^(${name}=)[ \\t]*$`, 'm');
|
|
246
|
+
if (line.test(contents)) return contents.replace(line, () => `${name}=${value}`);
|
|
247
|
+
if (new RegExp(`^${name}=`, 'm').test(contents)) return contents;
|
|
248
|
+
const separator = contents === '' || contents.endsWith('\n') ? '' : '\n';
|
|
249
|
+
return `${contents}${separator}${name}=${value}\n`;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
let contents = setEmpty(withLocalHeader(template), 'FRAZA_ENCRYPTION_KEY', key);
|
|
253
|
+
contents = contents.replace(/^NEXT_PUBLIC_URL=.*$/m, () => `NEXT_PUBLIC_URL=${publicUrl}`);
|
|
254
|
+
return setEmpty(contents, 'NEXT_PUBLIC_URL', publicUrl);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Crée `.env.example` (copie du modèle du core, suivie par git) et `.env.local`
|
|
259
|
+
* (la même copie remplie pour cette machine, ignorée par git). Un fichier qui
|
|
260
|
+
* existe déjà n'est jamais remplacé.
|
|
261
|
+
* @param {string} templatePath `node_modules/@fraza/core/.env.example`.
|
|
262
|
+
* @param {string} projectDir
|
|
263
|
+
* @returns {{ example: boolean, local: boolean }} Les fichiers créés.
|
|
264
|
+
*/
|
|
265
|
+
export function writeEnvFiles(templatePath, projectDir) {
|
|
266
|
+
const template = fs.readFileSync(templatePath, 'utf-8');
|
|
267
|
+
const examplePath = path.join(projectDir, '.env.example');
|
|
268
|
+
const localPath = path.join(projectDir, '.env.local');
|
|
269
|
+
const created = { example: false, local: false };
|
|
270
|
+
|
|
271
|
+
if (!fs.existsSync(examplePath)) {
|
|
272
|
+
fs.writeFileSync(examplePath, template);
|
|
273
|
+
created.example = true;
|
|
274
|
+
}
|
|
275
|
+
if (!fs.existsSync(localPath)) {
|
|
276
|
+
// 0600 : le fichier porte la clé de chiffrement du projet.
|
|
277
|
+
fs.writeFileSync(localPath, fillLocalEnv(template), { mode: 0o600 });
|
|
278
|
+
created.local = true;
|
|
279
|
+
}
|
|
280
|
+
return created;
|
|
281
|
+
}
|
package/bin/version.js
ADDED