@majordigital/create-acorn 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/create-acorn.mjs +424 -19
  2. package/package.json +1 -1
@@ -345,7 +345,7 @@ async function setupDato(projectName) {
345
345
  // Install DatoCMS dependencies
346
346
  console.log('Installing DatoCMS dependencies...');
347
347
  await runCommand('npm', ['install', '--legacy-peer-deps',
348
- '@datocms/cda-client', 'react-datocms',
348
+ '@datocms/cda-client', '@datocms/cma-client-node', 'react-datocms',
349
349
  'datocms-structured-text-to-plain-text', 'datocms-structured-text-utils',
350
350
  'graphql', 'graphql-request', 'dotenv'
351
351
  ]);
@@ -356,6 +356,79 @@ async function setupDato(projectName) {
356
356
  ]);
357
357
  console.log('');
358
358
 
359
+ // --- DatoCMS Project Connection & Model Scaffolding ---
360
+ const hasExisting = await ask('Do you already have a DatoCMS project? (y/n)', 'n');
361
+ const isExisting = hasExisting.toLowerCase() === 'y' || hasExisting.toLowerCase() === 'yes';
362
+
363
+ if (!isExisting) {
364
+ console.log('');
365
+ console.log('Please create a new project in the DatoCMS dashboard:');
366
+ console.log(' https://www.datocms.com/dashboard');
367
+ console.log('');
368
+ console.log('Once created, grab your Full-access API token from:');
369
+ console.log(' Settings > API Tokens > Full-access API token');
370
+ console.log('');
371
+ } else {
372
+ console.log('');
373
+ console.log('Great! Grab your Full-access API token from your DatoCMS project:');
374
+ console.log(' Settings > API Tokens > Full-access API token');
375
+ console.log('');
376
+ }
377
+
378
+ const apiToken = await ask('Paste your Full-access API token (or press Enter to skip)');
379
+
380
+ if (apiToken) {
381
+ console.log('');
382
+ console.log('Scaffolding DatoCMS models...');
383
+ console.log('');
384
+
385
+ try {
386
+ await scaffoldDatoModels(apiToken);
387
+ console.log('');
388
+ console.log('DatoCMS models created successfully!');
389
+ console.log('');
390
+
391
+ // Fetch the read-only CDA token for .env.local
392
+ let cdaToken = '';
393
+ try {
394
+ const { buildClient } = await import('@datocms/cma-client-node');
395
+ const client = buildClient({ apiToken });
396
+ const tokens = await client.accessTokens.list();
397
+ const readOnly = tokens.find(t => t.name === 'Read-only API token');
398
+ if (readOnly && readOnly.token) {
399
+ cdaToken = readOnly.token;
400
+ }
401
+ } catch {
402
+ // Could not fetch CDA token — user will add manually
403
+ }
404
+
405
+ // Write .env.local with tokens
406
+ const envLocalPath = join(process.cwd(), '.env.local');
407
+ writeFileSync(envLocalPath, `NEXT_DATOCMS_API_TOKEN=${cdaToken}
408
+ DATO_API_TOKEN=${apiToken}
409
+ NEXT_DATOCMS_ENVIRONMENT=draft
410
+ `);
411
+ if (cdaToken) {
412
+ console.log('Created .env.local with your API tokens.');
413
+ } else {
414
+ console.log('Created .env.local — add your Read-only API token to NEXT_DATOCMS_API_TOKEN.');
415
+ console.log('You can find it in Settings > API Tokens > Read-only API token.');
416
+ }
417
+ } catch (err) {
418
+ console.log('');
419
+ console.log('Warning: Could not scaffold DatoCMS models automatically.');
420
+ console.log(` Error: ${err.message}`);
421
+ console.log('');
422
+ console.log('You can create models manually in the DatoCMS dashboard.');
423
+ console.log('The GraphQL queries expect: Page, Hero (block), Button (block),');
424
+ console.log('Layout (singleton), MenuItem (block), CaseStudy, LegalPage, Post,');
425
+ console.log('CaseStudiesListing (singleton), PostListing (singleton).');
426
+ }
427
+ } else {
428
+ console.log('');
429
+ console.log('Skipping model scaffolding — you can set up models manually later.');
430
+ }
431
+
359
432
  // Update next.config.ts — replace Prismic image patterns with DatoCMS
360
433
  const nextConfigPath = join(process.cwd(), 'next.config.ts');
361
434
  try {
@@ -444,7 +517,6 @@ async function setupDato(projectName) {
444
517
  writeFileSync(envExamplePath, `NEXT_DATOCMS_API_TOKEN=
445
518
  DATO_API_TOKEN=
446
519
  NEXT_DATOCMS_ENVIRONMENT=draft
447
- NEXT_PUBLIC_SITE_URL=
448
520
  `);
449
521
  console.log('Updated .env.example with DatoCMS variables.');
450
522
  } catch {
@@ -456,24 +528,357 @@ NEXT_PUBLIC_SITE_URL=
456
528
  console.log('');
457
529
  console.log('=== Next Steps ===');
458
530
  console.log('');
459
- console.log(' 1. Go to https://www.datocms.com/dashboard and create a project (or use an existing one)');
460
- console.log('');
461
- console.log(' 2. Get your API token from Settings > API Tokens');
462
- console.log('');
463
- console.log(' 3. Add your token to .env.local:');
464
- console.log(' NEXT_DATOCMS_API_TOKEN=your_api_token');
465
- console.log(' NEXT_DATOCMS_ENVIRONMENT=draft');
466
- console.log('');
467
- console.log(' 4. Create your models in the DatoCMS dashboard');
468
- console.log('');
469
- console.log(' 5. Generate TypeScript types from your schema:');
470
- console.log(' npm run generate-types');
471
- console.log('');
472
- console.log(' 6. Start your dev server:');
531
+ if (!apiToken) {
532
+ console.log(' 1. Create a DatoCMS project at https://www.datocms.com/dashboard');
533
+ console.log('');
534
+ console.log(' 2. Get your API token from Settings > API Tokens');
535
+ console.log('');
536
+ console.log(' 3. Add your token to .env.local:');
537
+ console.log(' NEXT_DATOCMS_API_TOKEN=your_read_only_token');
538
+ console.log(' NEXT_DATOCMS_ENVIRONMENT=draft');
539
+ console.log('');
540
+ console.log(' 4. Create your models in the DatoCMS dashboard');
541
+ console.log('');
542
+ console.log(' 5. Generate TypeScript types from your schema:');
543
+ console.log(' npm run generate-types');
544
+ console.log('');
545
+ } else {
546
+ console.log(' 1. Generate TypeScript types from your schema:');
547
+ console.log(' npm run generate-types');
548
+ console.log('');
549
+ console.log(' 2. Start your dev server:');
550
+ console.log(' npm run dev');
551
+ console.log('');
552
+ console.log(' 3. Open your DatoCMS dashboard to add content to your models.');
553
+ console.log('');
554
+ }
555
+ console.log(' Start your dev server:');
473
556
  console.log(' npm run dev');
474
557
  console.log('');
475
558
  }
476
559
 
560
+ /**
561
+ * Scaffold default DatoCMS models using the Management API.
562
+ * Creates models that match the GraphQL queries in the dato-api layer.
563
+ */
564
+ async function scaffoldDatoModels(apiToken) {
565
+ const { buildClient } = await import('@datocms/cma-client-node');
566
+ const client = buildClient({ apiToken });
567
+
568
+ // --- Block models (must be created first so we can reference them) ---
569
+
570
+ // Button block
571
+ console.log(' Creating Button block...');
572
+ const buttonBlock = await client.itemTypes.create({
573
+ name: 'Button',
574
+ api_key: 'button',
575
+ modular_block: true,
576
+ });
577
+ await client.fields.create(buttonBlock.id, {
578
+ label: 'Text',
579
+ field_type: 'string',
580
+ api_key: 'text',
581
+ validators: { required: {} },
582
+ });
583
+ await client.fields.create(buttonBlock.id, {
584
+ label: 'Variant',
585
+ field_type: 'string',
586
+ api_key: 'variant',
587
+ validators: { enum: { values: ['primary', 'secondary', 'outline'] } },
588
+ appearance: {
589
+ addons: [],
590
+ editor: 'string_select',
591
+ parameters: { options: [
592
+ { hint: '', label: 'Primary', value: 'primary' },
593
+ { hint: '', label: 'Secondary', value: 'secondary' },
594
+ { hint: '', label: 'Outline', value: 'outline' },
595
+ ]},
596
+ },
597
+ });
598
+ await client.fields.create(buttonBlock.id, {
599
+ label: 'Link',
600
+ field_type: 'json',
601
+ api_key: 'link',
602
+ });
603
+
604
+ // Hero block
605
+ console.log(' Creating Hero block...');
606
+ const heroBlock = await client.itemTypes.create({
607
+ name: 'Hero',
608
+ api_key: 'hero',
609
+ modular_block: true,
610
+ });
611
+ await client.fields.create(heroBlock.id, {
612
+ label: 'Heading',
613
+ field_type: 'string',
614
+ api_key: 'heading',
615
+ validators: { required: {} },
616
+ });
617
+ await client.fields.create(heroBlock.id, {
618
+ label: 'Eyebrow',
619
+ field_type: 'string',
620
+ api_key: 'eyebrow',
621
+ });
622
+ await client.fields.create(heroBlock.id, {
623
+ label: 'Content',
624
+ field_type: 'text',
625
+ api_key: 'content',
626
+ appearance: {
627
+ addons: [],
628
+ editor: 'textarea',
629
+ parameters: {},
630
+ },
631
+ });
632
+ await client.fields.create(heroBlock.id, {
633
+ label: 'Layout',
634
+ field_type: 'string',
635
+ api_key: 'layout',
636
+ validators: { enum: { values: ['default', 'centered', 'split'] } },
637
+ appearance: {
638
+ addons: [],
639
+ editor: 'string_select',
640
+ parameters: { options: [
641
+ { hint: '', label: 'Default', value: 'default' },
642
+ { hint: '', label: 'Centered', value: 'centered' },
643
+ { hint: '', label: 'Split', value: 'split' },
644
+ ]},
645
+ },
646
+ });
647
+ await client.fields.create(heroBlock.id, {
648
+ label: 'Theme',
649
+ field_type: 'string',
650
+ api_key: 'theme',
651
+ validators: { enum: { values: ['light', 'dark'] } },
652
+ appearance: {
653
+ addons: [],
654
+ editor: 'string_select',
655
+ parameters: { options: [
656
+ { hint: '', label: 'Light', value: 'light' },
657
+ { hint: '', label: 'Dark', value: 'dark' },
658
+ ]},
659
+ },
660
+ });
661
+ await client.fields.create(heroBlock.id, {
662
+ label: 'Image',
663
+ field_type: 'file',
664
+ api_key: 'image',
665
+ });
666
+ await client.fields.create(heroBlock.id, {
667
+ label: 'Actions',
668
+ field_type: 'rich_text',
669
+ api_key: 'actions',
670
+ validators: {
671
+ rich_text_blocks: { item_types: [buttonBlock.id] },
672
+ },
673
+ });
674
+
675
+ // MenuItem block
676
+ console.log(' Creating MenuItem block...');
677
+ const menuItemBlock = await client.itemTypes.create({
678
+ name: 'Menu Item',
679
+ api_key: 'menu_item',
680
+ modular_block: true,
681
+ });
682
+ await client.fields.create(menuItemBlock.id, {
683
+ label: 'Heading',
684
+ field_type: 'string',
685
+ api_key: 'heading',
686
+ validators: { required: {} },
687
+ });
688
+
689
+ // --- Regular models ---
690
+
691
+ // Page model
692
+ console.log(' Creating Page model...');
693
+ const pageModel = await client.itemTypes.create({
694
+ name: 'Page',
695
+ api_key: 'page',
696
+ draft_mode_active: true,
697
+ });
698
+ await client.fields.create(pageModel.id, {
699
+ label: 'Heading',
700
+ field_type: 'string',
701
+ api_key: 'heading',
702
+ validators: { required: {} },
703
+ });
704
+ await client.fields.create(pageModel.id, {
705
+ label: 'Description',
706
+ field_type: 'text',
707
+ api_key: 'description',
708
+ });
709
+ await client.fields.create(pageModel.id, {
710
+ label: 'Slug',
711
+ field_type: 'slug',
712
+ api_key: 'slug',
713
+ validators: {
714
+ required: {},
715
+ slug_title_field: { title_field_id: (await client.fields.list(pageModel.id)).find(f => f.api_key === 'heading').id },
716
+ },
717
+ });
718
+ await client.fields.create(pageModel.id, {
719
+ label: 'Parent Page',
720
+ field_type: 'link',
721
+ api_key: 'parent_page',
722
+ validators: {
723
+ item_item_type: { on_publish_with_unpublished_references_strategy: 'fail', on_reference_unpublish_strategy: 'fail', on_reference_delete_strategy: 'set_null', item_types: [pageModel.id] },
724
+ },
725
+ });
726
+ await client.fields.create(pageModel.id, {
727
+ label: 'Body',
728
+ field_type: 'rich_text',
729
+ api_key: 'body',
730
+ validators: {
731
+ rich_text_blocks: { item_types: [heroBlock.id] },
732
+ },
733
+ });
734
+
735
+ // Add target link to MenuItem (now that Page exists)
736
+ await client.fields.create(menuItemBlock.id, {
737
+ label: 'Target',
738
+ field_type: 'link',
739
+ api_key: 'target',
740
+ validators: {
741
+ item_item_type: { on_publish_with_unpublished_references_strategy: 'fail', on_reference_unpublish_strategy: 'fail', on_reference_delete_strategy: 'set_null', item_types: [pageModel.id] },
742
+ },
743
+ });
744
+
745
+ // Case Study model
746
+ console.log(' Creating Case Study model...');
747
+ const caseStudyModel = await client.itemTypes.create({
748
+ name: 'Case Study',
749
+ api_key: 'case_study',
750
+ draft_mode_active: true,
751
+ });
752
+ await client.fields.create(caseStudyModel.id, {
753
+ label: 'Title',
754
+ field_type: 'string',
755
+ api_key: 'title',
756
+ validators: { required: {} },
757
+ });
758
+ await client.fields.create(caseStudyModel.id, {
759
+ label: 'Slug',
760
+ field_type: 'slug',
761
+ api_key: 'slug',
762
+ validators: {
763
+ required: {},
764
+ slug_title_field: { title_field_id: (await client.fields.list(caseStudyModel.id)).find(f => f.api_key === 'title').id },
765
+ },
766
+ });
767
+
768
+ // Legal Page model
769
+ console.log(' Creating Legal Page model...');
770
+ const legalPageModel = await client.itemTypes.create({
771
+ name: 'Legal Page',
772
+ api_key: 'legal_page',
773
+ draft_mode_active: true,
774
+ });
775
+ await client.fields.create(legalPageModel.id, {
776
+ label: 'Title',
777
+ field_type: 'string',
778
+ api_key: 'title',
779
+ validators: { required: {} },
780
+ });
781
+ await client.fields.create(legalPageModel.id, {
782
+ label: 'Slug',
783
+ field_type: 'slug',
784
+ api_key: 'slug',
785
+ validators: {
786
+ required: {},
787
+ slug_title_field: { title_field_id: (await client.fields.list(legalPageModel.id)).find(f => f.api_key === 'title').id },
788
+ },
789
+ });
790
+ await client.fields.create(legalPageModel.id, {
791
+ label: 'Download',
792
+ field_type: 'file',
793
+ api_key: 'download',
794
+ });
795
+
796
+ // Post model
797
+ console.log(' Creating Post model...');
798
+ const postModel = await client.itemTypes.create({
799
+ name: 'Post',
800
+ api_key: 'post',
801
+ draft_mode_active: true,
802
+ });
803
+ await client.fields.create(postModel.id, {
804
+ label: 'Title',
805
+ field_type: 'string',
806
+ api_key: 'title',
807
+ validators: { required: {} },
808
+ });
809
+ await client.fields.create(postModel.id, {
810
+ label: 'Slug',
811
+ field_type: 'slug',
812
+ api_key: 'slug',
813
+ validators: {
814
+ required: {},
815
+ slug_title_field: { title_field_id: (await client.fields.list(postModel.id)).find(f => f.api_key === 'title').id },
816
+ },
817
+ });
818
+
819
+ // Case Studies Listing (singleton)
820
+ console.log(' Creating Case Studies Listing singleton...');
821
+ const caseStudiesListing = await client.itemTypes.create({
822
+ name: 'Case Studies Listing',
823
+ api_key: 'case_studies_listing',
824
+ singleton: true,
825
+ });
826
+ await client.fields.create(caseStudiesListing.id, {
827
+ label: 'Slug',
828
+ field_type: 'string',
829
+ api_key: 'slug',
830
+ validators: { required: {} },
831
+ });
832
+
833
+ // Post Listing (singleton)
834
+ console.log(' Creating Post Listing singleton...');
835
+ const postListing = await client.itemTypes.create({
836
+ name: 'Post Listing',
837
+ api_key: 'post_listing',
838
+ singleton: true,
839
+ });
840
+ await client.fields.create(postListing.id, {
841
+ label: 'Slug',
842
+ field_type: 'string',
843
+ api_key: 'slug',
844
+ validators: { required: {} },
845
+ });
846
+
847
+ // Layout (singleton)
848
+ console.log(' Creating Layout singleton...');
849
+ const layoutModel = await client.itemTypes.create({
850
+ name: 'Layout',
851
+ api_key: 'layout',
852
+ singleton: true,
853
+ });
854
+ await client.fields.create(layoutModel.id, {
855
+ label: 'Navigation',
856
+ field_type: 'rich_text',
857
+ api_key: 'navigation',
858
+ validators: {
859
+ rich_text_blocks: { item_types: [menuItemBlock.id] },
860
+ },
861
+ });
862
+ await client.fields.create(layoutModel.id, {
863
+ label: 'Footer Navigation',
864
+ field_type: 'rich_text',
865
+ api_key: 'footer_navigation',
866
+ validators: {
867
+ rich_text_blocks: { item_types: [menuItemBlock.id] },
868
+ },
869
+ });
870
+ await client.fields.create(layoutModel.id, {
871
+ label: 'Copyright',
872
+ field_type: 'string',
873
+ api_key: 'copyright',
874
+ });
875
+
876
+ console.log('');
877
+ console.log(' Models created: Page, Case Study, Legal Page, Post');
878
+ console.log(' Singletons created: Layout, Case Studies Listing, Post Listing');
879
+ console.log(' Blocks created: Hero, Button, Menu Item');
880
+ }
881
+
477
882
  function generateReadme(projectName, cms) {
478
883
  const title = projectName || 'Project Title';
479
884
 
@@ -598,9 +1003,9 @@ This website is built using the [NextJS](https://nextjs.org/) framework, utilisi
598
1003
  Create \`.env.local\` file in the root directory:
599
1004
 
600
1005
  \`\`\`env
601
- DATOCMS_API_TOKEN=your_api_token
602
- DATOCMS_PREVIEW_SECRET=your_preview_secret
603
- SITE_URL=http://localhost:3000
1006
+ NEXT_DATOCMS_API_TOKEN=your_graphql_token
1007
+ DATO_API_TOKEN=your_readonly_token
1008
+ NEXT_DATOCMS_ENVIRONMENT="draft"
604
1009
  \`\`\`
605
1010
 
606
1011
  3. **Start the development server**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@majordigital/create-acorn",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Interactive scaffold for Acorn with Storyblok/Prismic/DatoCMS, TypeScript, and Tailwind.",
5
5
  "bin": {
6
6
  "create-acorn": "bin/create-acorn.mjs",