@donotdev/cli 0.0.8 → 0.0.9
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/dependencies-matrix.json +158 -74
- package/dist/bin/commands/create-app.js +43 -6
- package/dist/bin/commands/create-project.js +60 -7
- package/dist/bin/commands/deploy.js +111 -22
- package/dist/bin/dndev.js +7 -4
- package/dist/bin/donotdev.js +7 -4
- package/dist/index.js +174 -30
- package/package.json +4 -3
- package/templates/app-next/src/config/app.ts.example +1 -1
- package/templates/app-vite/index.html.example +24 -2
- package/templates/app-vite/src/config/app.ts.example +1 -1
- package/templates/app-vite/src/pages/FormPageExample.tsx.example +8 -5
- package/templates/app-vite/src/pages/ListPageExample.tsx.example +4 -7
- package/templates/root-consumer/.firebaserc.example +5 -0
- package/templates/root-consumer/entities/ExampleEntity.ts.example +2 -1
- package/templates/root-consumer/entities/demo.ts.example +1 -1
- package/templates/root-consumer/firestore.indexes.json.example +4 -0
- package/templates/root-consumer/firestore.rules.example +11 -0
- package/templates/root-consumer/guides/dndev/COMPONENTS_CRUD.md.example +9 -6
- package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +329 -57
- package/templates/root-consumer/guides/wai-way/entity_patterns.md.example +1 -1
- package/templates/root-consumer/storage.rules.example +8 -0
package/dist/index.js
CHANGED
|
@@ -8803,8 +8803,10 @@ function executeFirebaseCommand(args, options) {
|
|
|
8803
8803
|
errorOutput
|
|
8804
8804
|
};
|
|
8805
8805
|
}
|
|
8806
|
-
function buildFirebaseDeployArgs(
|
|
8807
|
-
const
|
|
8806
|
+
function buildFirebaseDeployArgs(deployTargets, projectId, debug, force) {
|
|
8807
|
+
const targets = Array.isArray(deployTargets) ? deployTargets : [deployTargets];
|
|
8808
|
+
const targetString = targets.join(",");
|
|
8809
|
+
const args = ["deploy", "--only", targetString, "--non-interactive"];
|
|
8808
8810
|
if (projectId) {
|
|
8809
8811
|
args.push("--project", projectId);
|
|
8810
8812
|
}
|
|
@@ -10212,6 +10214,47 @@ To fix this, run:
|
|
|
10212
10214
|
}
|
|
10213
10215
|
}
|
|
10214
10216
|
|
|
10217
|
+
// packages/tooling/src/apps/deploy-rules.ts
|
|
10218
|
+
init_utils();
|
|
10219
|
+
async function deployRules(appDir, serviceAccountPath, projectId, config, options) {
|
|
10220
|
+
const targets = [];
|
|
10221
|
+
if (options.firestore) {
|
|
10222
|
+
targets.push("firestore:rules");
|
|
10223
|
+
}
|
|
10224
|
+
if (options.firestoreIndexes) {
|
|
10225
|
+
targets.push("firestore:indexes");
|
|
10226
|
+
}
|
|
10227
|
+
if (options.storage) {
|
|
10228
|
+
targets.push("storage");
|
|
10229
|
+
}
|
|
10230
|
+
if (targets.length === 0) {
|
|
10231
|
+
return;
|
|
10232
|
+
}
|
|
10233
|
+
const targetNames = targets.join(", ");
|
|
10234
|
+
const s = Y2();
|
|
10235
|
+
s.start(`Deploying ${targetNames}...`);
|
|
10236
|
+
const args = buildFirebaseDeployArgs(targets, projectId, config.debug);
|
|
10237
|
+
const result = executeFirebaseCommand(args, {
|
|
10238
|
+
cwd: appDir,
|
|
10239
|
+
serviceAccountPath,
|
|
10240
|
+
projectId,
|
|
10241
|
+
debug: config.debug
|
|
10242
|
+
});
|
|
10243
|
+
if (result.error) {
|
|
10244
|
+
s.stop("Rules deployment failed");
|
|
10245
|
+
throw result.error;
|
|
10246
|
+
}
|
|
10247
|
+
if (!result.success) {
|
|
10248
|
+
s.stop("Rules deployment failed");
|
|
10249
|
+
handleDeploymentFailure(
|
|
10250
|
+
result,
|
|
10251
|
+
`firebase ${args.join(" ")}`,
|
|
10252
|
+
serviceAccountPath
|
|
10253
|
+
);
|
|
10254
|
+
}
|
|
10255
|
+
s.stop(`${targetNames} deployed successfully`);
|
|
10256
|
+
}
|
|
10257
|
+
|
|
10215
10258
|
// packages/tooling/src/apps/deploy-utils.ts
|
|
10216
10259
|
init_utils();
|
|
10217
10260
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
@@ -10332,6 +10375,9 @@ function validateFirebaseJson2(appDir) {
|
|
|
10332
10375
|
valid: false,
|
|
10333
10376
|
hasHosting: false,
|
|
10334
10377
|
hasFunctions: false,
|
|
10378
|
+
hasFirestoreRules: false,
|
|
10379
|
+
hasFirestoreIndexes: false,
|
|
10380
|
+
hasStorageRules: false,
|
|
10335
10381
|
errors: [`firebase.json not found at: ${firebaseJsonPath}`]
|
|
10336
10382
|
};
|
|
10337
10383
|
}
|
|
@@ -10344,11 +10390,17 @@ function validateFirebaseJson2(appDir) {
|
|
|
10344
10390
|
}
|
|
10345
10391
|
const hasHosting = !!config.hosting;
|
|
10346
10392
|
const hasFunctions = !!config.functions && Array.isArray(config.functions);
|
|
10393
|
+
const hasFirestoreRules = !!config.firestore?.rules && pathExists(joinPath(appDir, config.firestore.rules));
|
|
10394
|
+
const hasFirestoreIndexes = !!config.firestore?.indexes && pathExists(joinPath(appDir, config.firestore.indexes));
|
|
10395
|
+
const hasStorageRules = !!config.storage?.rules && pathExists(joinPath(appDir, config.storage.rules));
|
|
10347
10396
|
return {
|
|
10348
10397
|
valid: true,
|
|
10349
10398
|
projectId: config.projectId,
|
|
10350
10399
|
hasHosting,
|
|
10351
10400
|
hasFunctions,
|
|
10401
|
+
hasFirestoreRules,
|
|
10402
|
+
hasFirestoreIndexes,
|
|
10403
|
+
hasStorageRules,
|
|
10352
10404
|
errors: []
|
|
10353
10405
|
};
|
|
10354
10406
|
} catch (error2) {
|
|
@@ -10356,6 +10408,9 @@ function validateFirebaseJson2(appDir) {
|
|
|
10356
10408
|
valid: false,
|
|
10357
10409
|
hasHosting: false,
|
|
10358
10410
|
hasFunctions: false,
|
|
10411
|
+
hasFirestoreRules: false,
|
|
10412
|
+
hasFirestoreIndexes: false,
|
|
10413
|
+
hasStorageRules: false,
|
|
10359
10414
|
errors: [
|
|
10360
10415
|
`Invalid JSON format: ${error2 instanceof DoNotDevError ? error2.message : error2 instanceof Error ? error2.message : String(error2)}`
|
|
10361
10416
|
]
|
|
@@ -10520,16 +10575,36 @@ async function main5(options = {}) {
|
|
|
10520
10575
|
hint: "Deploy cloud functions"
|
|
10521
10576
|
});
|
|
10522
10577
|
}
|
|
10578
|
+
const hasRules = firebaseConfig2.hasFirestoreRules || firebaseConfig2.hasFirestoreIndexes || firebaseConfig2.hasStorageRules;
|
|
10579
|
+
if (hasRules) {
|
|
10580
|
+
choices.push({
|
|
10581
|
+
title: "Rules only",
|
|
10582
|
+
value: "rules",
|
|
10583
|
+
hint: "Deploy Firestore/Storage rules"
|
|
10584
|
+
});
|
|
10585
|
+
}
|
|
10523
10586
|
if (firebaseConfig2.hasHosting && firebaseConfig2.hasFunctions) {
|
|
10524
10587
|
choices.push({
|
|
10525
|
-
title: "
|
|
10588
|
+
title: "Frontend + Functions",
|
|
10526
10589
|
value: "both",
|
|
10527
|
-
hint: "Deploy
|
|
10590
|
+
hint: "Deploy hosting and functions"
|
|
10591
|
+
});
|
|
10592
|
+
}
|
|
10593
|
+
const deployableKindsCount = [
|
|
10594
|
+
firebaseConfig2.hasHosting,
|
|
10595
|
+
firebaseConfig2.hasFunctions,
|
|
10596
|
+
hasRules
|
|
10597
|
+
].filter(Boolean).length;
|
|
10598
|
+
if (deployableKindsCount > 1) {
|
|
10599
|
+
choices.push({
|
|
10600
|
+
title: "All",
|
|
10601
|
+
value: "all",
|
|
10602
|
+
hint: "Deploy everything (hosting, functions, rules)"
|
|
10528
10603
|
});
|
|
10529
10604
|
}
|
|
10530
10605
|
if (choices.length === 0) {
|
|
10531
10606
|
log.error(
|
|
10532
|
-
"No deployment targets found. firebase.json must have hosting or
|
|
10607
|
+
"No deployment targets found. firebase.json must have hosting, functions, or rules configuration."
|
|
10533
10608
|
);
|
|
10534
10609
|
process.exit(1);
|
|
10535
10610
|
}
|
|
@@ -10574,13 +10649,28 @@ async function main5(options = {}) {
|
|
|
10574
10649
|
showFirebaseJsonError(firebaseConfig.errors, appDir, deploymentType);
|
|
10575
10650
|
process.exit(1);
|
|
10576
10651
|
}
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10652
|
+
const shouldDeployFrontend = (deploymentType === "frontend" || deploymentType === "both" || deploymentType === "all") && firebaseConfig.hasHosting;
|
|
10653
|
+
const shouldDeployFunctions = (deploymentType === "functions" || deploymentType === "both" || deploymentType === "all") && firebaseConfig.hasFunctions;
|
|
10654
|
+
const shouldDeployRules = (deploymentType === "rules" || deploymentType === "all") && (firebaseConfig.hasFirestoreRules || firebaseConfig.hasFirestoreIndexes || firebaseConfig.hasStorageRules);
|
|
10655
|
+
if (deploymentType === "frontend" && !firebaseConfig.hasHosting) {
|
|
10656
|
+
log.error(
|
|
10657
|
+
"firebase.json does not contain hosting configuration, but frontend deployment was requested."
|
|
10658
|
+
);
|
|
10659
|
+
process.exit(1);
|
|
10660
|
+
}
|
|
10661
|
+
if (deploymentType === "functions" && !firebaseConfig.hasFunctions) {
|
|
10662
|
+
log.error(
|
|
10663
|
+
"firebase.json does not contain functions configuration, but functions deployment was requested."
|
|
10664
|
+
);
|
|
10665
|
+
process.exit(1);
|
|
10666
|
+
}
|
|
10667
|
+
if (deploymentType === "rules" && !shouldDeployRules) {
|
|
10668
|
+
log.error(
|
|
10669
|
+
"firebase.json does not contain rules configuration, but rules deployment was requested."
|
|
10670
|
+
);
|
|
10671
|
+
process.exit(1);
|
|
10672
|
+
}
|
|
10673
|
+
if (shouldDeployFrontend) {
|
|
10584
10674
|
const buildStatus = validateBuild(appDir);
|
|
10585
10675
|
let shouldBuild = false;
|
|
10586
10676
|
if (!buildStatus.exists || buildStatus.isEmpty) {
|
|
@@ -10623,14 +10713,6 @@ async function main5(options = {}) {
|
|
|
10623
10713
|
}
|
|
10624
10714
|
}
|
|
10625
10715
|
}
|
|
10626
|
-
if (deploymentType === "functions" || deploymentType === "both") {
|
|
10627
|
-
if (!firebaseConfig.hasFunctions) {
|
|
10628
|
-
log.error(
|
|
10629
|
-
"firebase.json does not contain functions configuration, but functions deployment was requested."
|
|
10630
|
-
);
|
|
10631
|
-
process.exit(1);
|
|
10632
|
-
}
|
|
10633
|
-
}
|
|
10634
10716
|
clearFirebaseCache(appDir);
|
|
10635
10717
|
Me(
|
|
10636
10718
|
`App: ${appName}
|
|
@@ -10639,10 +10721,10 @@ Deployment: ${deploymentType}
|
|
|
10639
10721
|
Service Account: ${serviceAccountResult.info.clientEmail}`,
|
|
10640
10722
|
"Deployment Configuration"
|
|
10641
10723
|
);
|
|
10642
|
-
if (
|
|
10724
|
+
if (shouldDeployFrontend) {
|
|
10643
10725
|
await deployFrontend(appDir, serviceAccountPath, config.project, config);
|
|
10644
10726
|
}
|
|
10645
|
-
if (
|
|
10727
|
+
if (shouldDeployFunctions) {
|
|
10646
10728
|
await deployFunctions(
|
|
10647
10729
|
appDir,
|
|
10648
10730
|
serviceAccountPath,
|
|
@@ -10650,6 +10732,13 @@ Service Account: ${serviceAccountResult.info.clientEmail}`,
|
|
|
10650
10732
|
config
|
|
10651
10733
|
);
|
|
10652
10734
|
}
|
|
10735
|
+
if (shouldDeployRules) {
|
|
10736
|
+
await deployRules(appDir, serviceAccountPath, config.project, config, {
|
|
10737
|
+
firestore: firebaseConfig.hasFirestoreRules,
|
|
10738
|
+
firestoreIndexes: firebaseConfig.hasFirestoreIndexes,
|
|
10739
|
+
storage: firebaseConfig.hasStorageRules
|
|
10740
|
+
});
|
|
10741
|
+
}
|
|
10653
10742
|
Se("Deployment completed successfully!");
|
|
10654
10743
|
} catch (error2) {
|
|
10655
10744
|
if (error2 instanceof DoNotDevError) {
|
|
@@ -11180,7 +11269,7 @@ function generatePackageJson(templateName, mode, options = {}) {
|
|
|
11180
11269
|
"dependencies-matrix.json not found. This command requires the matrix file."
|
|
11181
11270
|
);
|
|
11182
11271
|
}
|
|
11183
|
-
const { matrix
|
|
11272
|
+
const { matrix } = matrixResult;
|
|
11184
11273
|
const template = matrix.templateMapping?.[templateName];
|
|
11185
11274
|
if (!template) {
|
|
11186
11275
|
throw new Error(`Template "${templateName}" not found in matrix`);
|
|
@@ -11244,6 +11333,7 @@ function generatePackageJson(templateName, mode, options = {}) {
|
|
|
11244
11333
|
}
|
|
11245
11334
|
}
|
|
11246
11335
|
if (templateName.includes("functions")) {
|
|
11336
|
+
result.main = "lib/index.js";
|
|
11247
11337
|
result.engines = { node: "20" };
|
|
11248
11338
|
if (options.appName) {
|
|
11249
11339
|
const platform = templateName.includes("vercel") ? "Vercel" : "Firebase";
|
|
@@ -11371,6 +11461,8 @@ async function createApp(appName, appConfig, workspaceRoot, templatesRoot) {
|
|
|
11371
11461
|
s.start(`Creating app: ${appName}`);
|
|
11372
11462
|
await ensureDir(appDir);
|
|
11373
11463
|
const templateDir = appTemplate === "demo" ? "app-demo" : appTemplate === "nextjs" ? "app-next" : "app-vite";
|
|
11464
|
+
const firebaseProjectId = (appConfig?.firebaseProjectId ?? "").trim() || appName.toLowerCase().replace(/\s+/g, "-");
|
|
11465
|
+
const firebaseRegion = appConfig?.firebaseRegion ?? "europe-west1";
|
|
11374
11466
|
const replacements = {
|
|
11375
11467
|
projectName: appName,
|
|
11376
11468
|
appName,
|
|
@@ -11379,11 +11471,14 @@ async function createApp(appName, appConfig, workspaceRoot, templatesRoot) {
|
|
|
11379
11471
|
needsCRUD: Boolean(appConfig.needsCRUD),
|
|
11380
11472
|
setupGithubActions: false,
|
|
11381
11473
|
appNames: [appName],
|
|
11382
|
-
firebaseProjectId
|
|
11474
|
+
firebaseProjectId,
|
|
11475
|
+
firebaseRegion,
|
|
11383
11476
|
firebaseSecretName: appName.toUpperCase().replace(/-/g, "_"),
|
|
11384
11477
|
monorepoRelativePath: "../../packages/tooling",
|
|
11385
11478
|
appTemplate,
|
|
11386
|
-
isNextjs: appTemplate === "nextjs"
|
|
11479
|
+
isNextjs: appTemplate === "nextjs",
|
|
11480
|
+
YOUR_FIREBASE_PROJECT_ID: firebaseProjectId,
|
|
11481
|
+
YOUR_REGION: firebaseRegion
|
|
11387
11482
|
};
|
|
11388
11483
|
const templateSourceDir = joinPath(templatesRoot, templateDir);
|
|
11389
11484
|
const templateFiles = await glob("**/*", {
|
|
@@ -11476,6 +11571,32 @@ async function createApp(appName, appConfig, workspaceRoot, templatesRoot) {
|
|
|
11476
11571
|
await replacePlaceholders(firebaseJsonDest, replacements);
|
|
11477
11572
|
}
|
|
11478
11573
|
}
|
|
11574
|
+
const firebasercSource = joinPath(
|
|
11575
|
+
deploymentTemplateDir,
|
|
11576
|
+
".firebaserc.example"
|
|
11577
|
+
);
|
|
11578
|
+
if (pathExists(firebasercSource)) {
|
|
11579
|
+
const firebasercDest = joinPath(appDir, ".firebaserc");
|
|
11580
|
+
await copy(firebasercSource, firebasercDest);
|
|
11581
|
+
if (await isTextFile(firebasercDest)) {
|
|
11582
|
+
await replacePlaceholders(firebasercDest, replacements);
|
|
11583
|
+
}
|
|
11584
|
+
}
|
|
11585
|
+
if (appConfig.needsBackend && appConfig.backendPlatform === "firebase") {
|
|
11586
|
+
const rulesFiles = [
|
|
11587
|
+
"firestore.rules.example",
|
|
11588
|
+
"firestore.indexes.json.example",
|
|
11589
|
+
"storage.rules.example"
|
|
11590
|
+
];
|
|
11591
|
+
for (const example of rulesFiles) {
|
|
11592
|
+
const src = joinPath(deploymentTemplateDir, example);
|
|
11593
|
+
if (pathExists(src)) {
|
|
11594
|
+
const destName = example.replace(".example", "");
|
|
11595
|
+
const dest = joinPath(appDir, destName);
|
|
11596
|
+
await copy(src, dest);
|
|
11597
|
+
}
|
|
11598
|
+
}
|
|
11599
|
+
}
|
|
11479
11600
|
if (appTemplate === "nextjs" || appConfig.needsBackend && appConfig.backendPlatform === "vercel") {
|
|
11480
11601
|
const vercelJsonSource = joinPath(
|
|
11481
11602
|
deploymentTemplateDir,
|
|
@@ -11501,12 +11622,15 @@ async function createApp(appName, appConfig, workspaceRoot, templatesRoot) {
|
|
|
11501
11622
|
}
|
|
11502
11623
|
if (isInteractive) {
|
|
11503
11624
|
Se("\u{1F389} App created successfully!");
|
|
11625
|
+
const firebaseStep = appConfig.needsBackend && appConfig.backendPlatform === "firebase" ? `2. Set Firebase project: cd apps/${appName} && firebase use --add (or edit .firebase rc)
|
|
11626
|
+
3. bun install
|
|
11627
|
+
4. bun run dev` : `2. bun install
|
|
11628
|
+
3. bun run dev`;
|
|
11504
11629
|
Me(
|
|
11505
11630
|
`Next steps:
|
|
11506
11631
|
|
|
11507
11632
|
1. cd apps/${appName}
|
|
11508
|
-
|
|
11509
|
-
3. bun run dev
|
|
11633
|
+
${firebaseStep}
|
|
11510
11634
|
|
|
11511
11635
|
Happy coding!`,
|
|
11512
11636
|
"\u{1F4CB} Next Steps"
|
|
@@ -11538,7 +11662,9 @@ async function main7(options) {
|
|
|
11538
11662
|
selectedEntities: [],
|
|
11539
11663
|
userAuth: "social",
|
|
11540
11664
|
billing: true,
|
|
11541
|
-
features: []
|
|
11665
|
+
features: [],
|
|
11666
|
+
firebaseProjectId: options.firebaseProjectId,
|
|
11667
|
+
firebaseRegion: options.firebaseRegion
|
|
11542
11668
|
};
|
|
11543
11669
|
await createApp(appName, appConfig);
|
|
11544
11670
|
} else {
|
|
@@ -11878,6 +12004,8 @@ async function main8(options) {
|
|
|
11878
12004
|
overwrite: true
|
|
11879
12005
|
});
|
|
11880
12006
|
const rootTemplateDir = joinPath(templatesRoot, "root-consumer");
|
|
12007
|
+
const firebaseProjectId = projectName.toLowerCase().replace(/\s+/g, "-");
|
|
12008
|
+
const firebaseRegion = "europe-west1";
|
|
11881
12009
|
const rootReplacements = {
|
|
11882
12010
|
projectName,
|
|
11883
12011
|
appNames: isMergeMode ? allAppNames : appNames,
|
|
@@ -11886,12 +12014,22 @@ async function main8(options) {
|
|
|
11886
12014
|
monorepoRelativePath: relativeMonorepoPath,
|
|
11887
12015
|
appTemplate: "vite",
|
|
11888
12016
|
isNextjs: false,
|
|
11889
|
-
firebaseProjectId
|
|
12017
|
+
firebaseProjectId,
|
|
12018
|
+
firebaseRegion,
|
|
11890
12019
|
firebaseSecretName: projectName.toUpperCase().replace(/-/g, "_"),
|
|
12020
|
+
YOUR_FIREBASE_PROJECT_ID: firebaseProjectId,
|
|
12021
|
+
YOUR_REGION: firebaseRegion,
|
|
11891
12022
|
needsAuth,
|
|
11892
12023
|
needsOAuth,
|
|
11893
12024
|
needsBilling
|
|
11894
12025
|
};
|
|
12026
|
+
const firebaseRootFiles = /* @__PURE__ */ new Set([
|
|
12027
|
+
"firebase.json.example",
|
|
12028
|
+
".firebaserc.example",
|
|
12029
|
+
"firestore.rules.example",
|
|
12030
|
+
"firestore.indexes.json.example",
|
|
12031
|
+
"storage.rules.example"
|
|
12032
|
+
]);
|
|
11895
12033
|
const files = await glob("**/*", {
|
|
11896
12034
|
cwd: rootTemplateDir,
|
|
11897
12035
|
dot: true,
|
|
@@ -11899,6 +12037,7 @@ async function main8(options) {
|
|
|
11899
12037
|
});
|
|
11900
12038
|
for (const file of files) {
|
|
11901
12039
|
if (file === "package.json.example") continue;
|
|
12040
|
+
if (firebaseRootFiles.has(file)) continue;
|
|
11902
12041
|
const sourcePath = joinPath(rootTemplateDir, file);
|
|
11903
12042
|
let destFileName = file;
|
|
11904
12043
|
if (destFileName.endsWith(".example")) {
|
|
@@ -11973,6 +12112,8 @@ async function main8(options) {
|
|
|
11973
12112
|
}
|
|
11974
12113
|
}
|
|
11975
12114
|
if (installDemoApp) {
|
|
12115
|
+
const demoFirebaseProjectId = projectName.toLowerCase().replace(/\s+/g, "-");
|
|
12116
|
+
const demoFirebaseRegion = "europe-west1";
|
|
11976
12117
|
await copyTemplateFiles(demoTemplateDir, demoAppDir, {
|
|
11977
12118
|
projectName,
|
|
11978
12119
|
appName: "demo",
|
|
@@ -11980,8 +12121,11 @@ async function main8(options) {
|
|
|
11980
12121
|
needsCRUD: false,
|
|
11981
12122
|
setupGithubActions: false,
|
|
11982
12123
|
appNames,
|
|
11983
|
-
firebaseProjectId:
|
|
12124
|
+
firebaseProjectId: demoFirebaseProjectId,
|
|
12125
|
+
firebaseRegion: demoFirebaseRegion,
|
|
11984
12126
|
firebaseSecretName: projectName.toUpperCase().replace(/-/g, "_"),
|
|
12127
|
+
YOUR_FIREBASE_PROJECT_ID: demoFirebaseProjectId,
|
|
12128
|
+
YOUR_REGION: demoFirebaseRegion,
|
|
11985
12129
|
monorepoRelativePath: executionMode === "development" ? calculateRelativePath(projectDirNormalized, monorepoRoot) : "",
|
|
11986
12130
|
appTemplate: "demo"
|
|
11987
12131
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@donotdev/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Command-line interface for DoNotDev Framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@clack/prompts": "^0.11.0",
|
|
37
|
-
"commander": "^14.0.
|
|
37
|
+
"commander": "^14.0.3",
|
|
38
38
|
"fast-glob": "^3.3.3"
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
@@ -59,5 +59,6 @@
|
|
|
59
59
|
"publishConfig": {
|
|
60
60
|
"registry": "https://registry.npmjs.org",
|
|
61
61
|
"access": "public"
|
|
62
|
-
}
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {}
|
|
63
64
|
}
|
|
@@ -29,7 +29,7 @@ export const appConfig: AppConfig = {
|
|
|
29
29
|
name: APP_NAME,
|
|
30
30
|
shortName: APP_SHORT_NAME,
|
|
31
31
|
description: APP_DESCRIPTION,
|
|
32
|
-
//
|
|
32
|
+
// Note: URL comes from NEXT_PUBLIC_APP_URL in .env, not here
|
|
33
33
|
|
|
34
34
|
// Footer legal links - remove any you don't need
|
|
35
35
|
footer: {
|
|
@@ -16,8 +16,13 @@
|
|
|
16
16
|
<!-- ✅ PWA: Manifest link (if exists) -->
|
|
17
17
|
<link rel="manifest" href="/manifest.json" />
|
|
18
18
|
|
|
19
|
-
<!-- ✅ PERFORMANCE:
|
|
20
|
-
<link rel="
|
|
19
|
+
<!-- ✅ PERFORMANCE: Preload critical fonts (non-blocking) -->
|
|
20
|
+
<link rel="preload" href="/fonts/Inter-latin.woff2" as="font" type="font/woff2" crossorigin="anonymous">
|
|
21
|
+
<link rel="preload" href="/fonts/Roboto-400-latin.woff2" as="font" type="font/woff2" crossorigin="anonymous">
|
|
22
|
+
|
|
23
|
+
<!-- ✅ PERFORMANCE: Load extended font subsets async (non-blocking) -->
|
|
24
|
+
<link rel="stylesheet" href="/fonts/fonts.css" media="print" onload="this.media='all'">
|
|
25
|
+
<noscript><link rel="stylesheet" href="/fonts/fonts.css"></noscript>
|
|
21
26
|
|
|
22
27
|
<!-- ✅ PERFORMANCE: Preconnect to external domains (OAuth providers) -->
|
|
23
28
|
<!-- GitHub OAuth -->
|
|
@@ -36,6 +41,23 @@
|
|
|
36
41
|
|
|
37
42
|
<!-- ✅ PERFORMANCE: Critical CSS inlined here by build -->
|
|
38
43
|
<style>
|
|
44
|
+
/* Critical @font-face declarations - must be inline for preloaded fonts to work */
|
|
45
|
+
@font-face {
|
|
46
|
+
font-family: Inter;
|
|
47
|
+
font-style: normal;
|
|
48
|
+
font-weight: 400 700;
|
|
49
|
+
font-display: swap;
|
|
50
|
+
src: url('/fonts/Inter-latin.woff2') format('woff2');
|
|
51
|
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
52
|
+
}
|
|
53
|
+
@font-face {
|
|
54
|
+
font-family: Roboto;
|
|
55
|
+
font-style: normal;
|
|
56
|
+
font-weight: 400;
|
|
57
|
+
font-display: swap;
|
|
58
|
+
src: url('/fonts/Roboto-400-latin.woff2') format('woff2');
|
|
59
|
+
unicode-range: U+0000-00FF;
|
|
60
|
+
}
|
|
39
61
|
/* Critical above-the-fold styles */
|
|
40
62
|
html, body {
|
|
41
63
|
margin: 0;
|
|
@@ -29,7 +29,7 @@ export const appConfig: AppConfig = {
|
|
|
29
29
|
name: APP_NAME,
|
|
30
30
|
shortName: APP_SHORT_NAME,
|
|
31
31
|
description: APP_DESCRIPTION,
|
|
32
|
-
//
|
|
32
|
+
// Note: URL comes from VITE_APP_URL in .env, not here
|
|
33
33
|
|
|
34
34
|
// Footer legal links - remove any you don't need
|
|
35
35
|
footer: {
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
import { useEffect, useState } from 'react';
|
|
14
14
|
|
|
15
15
|
import { Section, Button, Alert } from '@donotdev/components';
|
|
16
|
-
import {
|
|
16
|
+
import { useCrud } from '@donotdev/crud';
|
|
17
17
|
import { useTranslation } from '@donotdev/core';
|
|
18
18
|
import type { PageMeta } from '@donotdev/core';
|
|
19
|
-
import { PageContainer, Link,
|
|
19
|
+
import { PageContainer, Link, EntityFormRenderer } from '@donotdev/ui';
|
|
20
20
|
|
|
21
21
|
// Import your entity from root-level entities folder
|
|
22
22
|
// import { productEntity } from 'entities/Product';
|
|
@@ -51,7 +51,6 @@ export const meta: PageMeta = {
|
|
|
51
51
|
export default function ProductPage() {
|
|
52
52
|
const { t } = useTranslation(NAMESPACE);
|
|
53
53
|
const id = useParam('id');
|
|
54
|
-
const navigate = useNavigate();
|
|
55
54
|
const isNew = id === 'new';
|
|
56
55
|
|
|
57
56
|
// useCrud provides CRUD operations with optimistic updates
|
|
@@ -84,8 +83,8 @@ export default function ProductPage() {
|
|
|
84
83
|
update(id, data); // No await - fires in background
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
//
|
|
88
|
-
navigate
|
|
86
|
+
// Navigation happens automatically via cancelPath (defaults to /products)
|
|
87
|
+
// Or you can navigate manually if needed
|
|
89
88
|
};
|
|
90
89
|
|
|
91
90
|
// ==========================================================================
|
|
@@ -121,6 +120,8 @@ export default function ProductPage() {
|
|
|
121
120
|
onSubmit={handleSubmit}
|
|
122
121
|
defaultValues={{ status: 'draft' }} // Initial values for new items
|
|
123
122
|
submitText={t('create')}
|
|
123
|
+
// Cancel automatically navigates to /products (or cancelPath if provided)
|
|
124
|
+
cancelPath="/products"
|
|
124
125
|
/>
|
|
125
126
|
) : (
|
|
126
127
|
// EDIT MODE
|
|
@@ -130,6 +131,8 @@ export default function ProductPage() {
|
|
|
130
131
|
onSubmit={handleSubmit}
|
|
131
132
|
defaultValues={formData} // Loaded data
|
|
132
133
|
submitText={t('update')}
|
|
134
|
+
// Cancel automatically navigates to /products (or cancelPath if provided)
|
|
135
|
+
cancelPath="/products"
|
|
133
136
|
/>
|
|
134
137
|
)}
|
|
135
138
|
</Section>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { Package } from 'lucide-react';
|
|
16
16
|
|
|
17
|
-
import { EntityList } from '@donotdev/
|
|
17
|
+
import { EntityList } from '@donotdev/ui';
|
|
18
18
|
import { useAuth } from '@donotdev/auth';
|
|
19
19
|
import type { PageMeta } from '@donotdev/core';
|
|
20
20
|
import { PageContainer } from '@donotdev/ui';
|
|
@@ -51,17 +51,14 @@ export default function ProductsListPage() {
|
|
|
51
51
|
// - Applies visibility rules based on user role
|
|
52
52
|
// - Includes search, sort, pagination
|
|
53
53
|
// - Add/Edit/Delete buttons with proper permissions
|
|
54
|
+
// - Automatic routing: edit/view -> /products/:id, create -> /products/new
|
|
54
55
|
|
|
55
56
|
return (
|
|
56
57
|
<PageContainer>
|
|
57
58
|
<EntityList
|
|
58
59
|
entity={productEntity}
|
|
59
60
|
userRole={user?.role}
|
|
60
|
-
// Optional
|
|
61
|
-
// onRowClick={(item) => navigate(`/products/${item.id}`)}
|
|
62
|
-
// createPath="/products/new"
|
|
63
|
-
// hideActions={false}
|
|
64
|
-
// pageSize={25}
|
|
61
|
+
// Optional: basePath="/admin/products" or onClick={(id) => openSheet(id)}
|
|
65
62
|
/>
|
|
66
63
|
</PageContainer>
|
|
67
64
|
);
|
|
@@ -73,7 +70,7 @@ export default function ProductsListPage() {
|
|
|
73
70
|
//
|
|
74
71
|
// For a card-based grid instead of table:
|
|
75
72
|
//
|
|
76
|
-
// import { EntityCardList } from '@donotdev/
|
|
73
|
+
// import { EntityCardList } from '@donotdev/ui';
|
|
77
74
|
//
|
|
78
75
|
// export default function ProductsGridPage() {
|
|
79
76
|
// return (
|
|
@@ -63,6 +63,7 @@ export const productEntity = defineEntity({
|
|
|
63
63
|
// 'checkbox' → Multiple selections
|
|
64
64
|
// 'date' → Date picker
|
|
65
65
|
// 'timestamp' → Date + time
|
|
66
|
+
// 'price' → Structured price (amount, currency, VAT, discount %)
|
|
66
67
|
// 'images' → Image upload (multiple)
|
|
67
68
|
// 'reference' → Link to another entity
|
|
68
69
|
//
|
|
@@ -120,7 +121,7 @@ export const productEntity = defineEntity({
|
|
|
120
121
|
price: {
|
|
121
122
|
name: 'price',
|
|
122
123
|
label: 'price',
|
|
123
|
-
type: '
|
|
124
|
+
type: 'price',
|
|
124
125
|
visibility: 'guest',
|
|
125
126
|
editable: 'admin',
|
|
126
127
|
validation: { required: true },
|
|
@@ -99,7 +99,7 @@ const demoEntity = defineEntity({
|
|
|
99
99
|
price: {
|
|
100
100
|
name: 'price',
|
|
101
101
|
label: 'price',
|
|
102
|
-
type: '
|
|
102
|
+
type: 'price', // Value: { amount, currency?, vatIncluded?, discountPercent? }
|
|
103
103
|
visibility: 'guest',
|
|
104
104
|
editable: 'admin',
|
|
105
105
|
validation: { required: true },
|
|
@@ -19,14 +19,14 @@ await remove('doc-id');
|
|
|
19
19
|
Paginated list with automatic loading. For data tables.
|
|
20
20
|
|
|
21
21
|
```tsx
|
|
22
|
-
const { items, loading,
|
|
22
|
+
const { items, loading, refresh } = useCrudList(productEntity);
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
### useCrudCardList
|
|
26
26
|
Card-based list with infinite scroll.
|
|
27
27
|
|
|
28
28
|
```tsx
|
|
29
|
-
const { items, loading,
|
|
29
|
+
const { items, loading, refresh } = useCrudCardList(articleEntity);
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
---
|
|
@@ -106,6 +106,9 @@ Field components are auto-rendered by `FormFieldRenderer`. You don't import them
|
|
|
106
106
|
### Numbers
|
|
107
107
|
- `number` - Numeric input
|
|
108
108
|
- `range` - Slider input
|
|
109
|
+
- `rating` - Star rating input (1–5, configurable max)
|
|
110
|
+
|
|
111
|
+
**Rating + comment (e.g. reviews):** Use two fields on the same entity — `rating` (type `rating`) for stars and `comment` (type `textarea`) for the text. The form renders them as separate rows; no composite field type needed.
|
|
109
112
|
|
|
110
113
|
### Boolean
|
|
111
114
|
- `checkbox` - Checkbox input
|
|
@@ -153,7 +156,7 @@ import { useController, registerFieldType } from '@donotdev/crud';
|
|
|
153
156
|
import type { ControlledFieldProps } from '@donotdev/crud';
|
|
154
157
|
|
|
155
158
|
// Custom controlled component MUST use framework's useController (not react-hook-form's)
|
|
156
|
-
function
|
|
159
|
+
function ScoreField({
|
|
157
160
|
fieldConfig,
|
|
158
161
|
control,
|
|
159
162
|
errors,
|
|
@@ -179,7 +182,7 @@ function RatingField({
|
|
|
179
182
|
onChange?.(value);
|
|
180
183
|
}}
|
|
181
184
|
min={0}
|
|
182
|
-
max={
|
|
185
|
+
max={10}
|
|
183
186
|
/>
|
|
184
187
|
{fieldState?.error && (
|
|
185
188
|
<span className="error">{fieldState.error.message}</span>
|
|
@@ -189,8 +192,8 @@ function RatingField({
|
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
registerFieldType({
|
|
192
|
-
type: '
|
|
193
|
-
controlledComponent:
|
|
195
|
+
type: 'score',
|
|
196
|
+
controlledComponent: ScoreField,
|
|
194
197
|
});
|
|
195
198
|
```
|
|
196
199
|
|