@majordigital/create-acorn 1.4.1 → 1.5.1
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/bin/create-acorn.mjs +427 -15
- package/package.json +1 -1
package/bin/create-acorn.mjs
CHANGED
|
@@ -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,85 @@ 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('NOTE: DatoCMS does not support creating projects via CLI or API.');
|
|
366
|
+
console.log('You need to create the project manually first:');
|
|
367
|
+
console.log('');
|
|
368
|
+
console.log(' 1. Go to https://www.datocms.com/dashboard');
|
|
369
|
+
console.log(' 2. Click "New Project" and create your project');
|
|
370
|
+
console.log(' 3. Go to Settings > API Tokens > Full-access API token');
|
|
371
|
+
console.log(' 4. Copy the token and paste it below');
|
|
372
|
+
console.log('');
|
|
373
|
+
console.log('If you provide the token, we will automatically scaffold all models');
|
|
374
|
+
console.log('(Page, Hero, Button, Layout, etc.) in your DatoCMS project.');
|
|
375
|
+
console.log('');
|
|
376
|
+
} else {
|
|
377
|
+
console.log('');
|
|
378
|
+
console.log('Great! We can automatically scaffold all models in your project.');
|
|
379
|
+
console.log('Grab your Full-access API token from:');
|
|
380
|
+
console.log(' Settings > API Tokens > Full-access API token');
|
|
381
|
+
console.log('');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const apiToken = await ask('Paste your Full-access API token (or press Enter to skip)');
|
|
385
|
+
|
|
386
|
+
if (apiToken) {
|
|
387
|
+
console.log('');
|
|
388
|
+
console.log('Scaffolding DatoCMS models...');
|
|
389
|
+
console.log('');
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
await scaffoldDatoModels(apiToken);
|
|
393
|
+
console.log('');
|
|
394
|
+
console.log('DatoCMS models created successfully!');
|
|
395
|
+
console.log('');
|
|
396
|
+
|
|
397
|
+
// Fetch the read-only CDA token for .env.local
|
|
398
|
+
let cdaToken = '';
|
|
399
|
+
try {
|
|
400
|
+
const { buildClient } = await import('@datocms/cma-client-node');
|
|
401
|
+
const client = buildClient({ apiToken });
|
|
402
|
+
const tokens = await client.accessTokens.list();
|
|
403
|
+
const readOnly = tokens.find(t => t.name === 'Read-only API token');
|
|
404
|
+
if (readOnly?.token) {
|
|
405
|
+
cdaToken = readOnly.token;
|
|
406
|
+
}
|
|
407
|
+
} catch {
|
|
408
|
+
// Could not fetch CDA token — user will add manually
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Write .env.local with tokens
|
|
412
|
+
const envLocalPath = join(process.cwd(), '.env.local');
|
|
413
|
+
writeFileSync(envLocalPath, `NEXT_DATOCMS_API_TOKEN=${cdaToken}
|
|
414
|
+
DATO_API_TOKEN=${apiToken}
|
|
415
|
+
NEXT_DATOCMS_ENVIRONMENT=draft
|
|
416
|
+
`);
|
|
417
|
+
if (cdaToken) {
|
|
418
|
+
console.log('Created .env.local with your API tokens.');
|
|
419
|
+
} else {
|
|
420
|
+
console.log('Created .env.local — add your Read-only API token to NEXT_DATOCMS_API_TOKEN.');
|
|
421
|
+
console.log('You can find it in Settings > API Tokens > Read-only API token.');
|
|
422
|
+
}
|
|
423
|
+
} catch (err) {
|
|
424
|
+
console.log('');
|
|
425
|
+
console.log('Warning: Could not scaffold DatoCMS models automatically.');
|
|
426
|
+
console.log(` Error: ${err.message}`);
|
|
427
|
+
console.log('');
|
|
428
|
+
console.log('You can create models manually in the DatoCMS dashboard.');
|
|
429
|
+
console.log('The GraphQL queries expect: Page, Hero (block), Button (block),');
|
|
430
|
+
console.log('Layout (singleton), MenuItem (block), CaseStudy, LegalPage, Post,');
|
|
431
|
+
console.log('CaseStudiesListing (singleton), PostListing (singleton).');
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
console.log('');
|
|
435
|
+
console.log('Skipping model scaffolding — you can set up models manually later.');
|
|
436
|
+
}
|
|
437
|
+
|
|
359
438
|
// Update next.config.ts — replace Prismic image patterns with DatoCMS
|
|
360
439
|
const nextConfigPath = join(process.cwd(), 'next.config.ts');
|
|
361
440
|
try {
|
|
@@ -455,24 +534,357 @@ NEXT_DATOCMS_ENVIRONMENT=draft
|
|
|
455
534
|
console.log('');
|
|
456
535
|
console.log('=== Next Steps ===');
|
|
457
536
|
console.log('');
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
537
|
+
if (!apiToken) {
|
|
538
|
+
console.log(' 1. Create a DatoCMS project at https://www.datocms.com/dashboard');
|
|
539
|
+
console.log('');
|
|
540
|
+
console.log(' 2. Get your API token from Settings > API Tokens');
|
|
541
|
+
console.log('');
|
|
542
|
+
console.log(' 3. Add your token to .env.local:');
|
|
543
|
+
console.log(' NEXT_DATOCMS_API_TOKEN=your_read_only_token');
|
|
544
|
+
console.log(' NEXT_DATOCMS_ENVIRONMENT=draft');
|
|
545
|
+
console.log('');
|
|
546
|
+
console.log(' 4. Create your models in the DatoCMS dashboard');
|
|
547
|
+
console.log('');
|
|
548
|
+
console.log(' 5. Generate TypeScript types from your schema:');
|
|
549
|
+
console.log(' npm run generate-types');
|
|
550
|
+
console.log('');
|
|
551
|
+
} else {
|
|
552
|
+
console.log(' 1. Generate TypeScript types from your schema:');
|
|
553
|
+
console.log(' npm run generate-types');
|
|
554
|
+
console.log('');
|
|
555
|
+
console.log(' 2. Start your dev server:');
|
|
556
|
+
console.log(' npm run dev');
|
|
557
|
+
console.log('');
|
|
558
|
+
console.log(' 3. Open your DatoCMS dashboard to add content to your models.');
|
|
559
|
+
console.log('');
|
|
560
|
+
}
|
|
561
|
+
console.log(' Start your dev server:');
|
|
472
562
|
console.log(' npm run dev');
|
|
473
563
|
console.log('');
|
|
474
564
|
}
|
|
475
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Scaffold default DatoCMS models using the Management API.
|
|
568
|
+
* Creates models that match the GraphQL queries in the dato-api layer.
|
|
569
|
+
*/
|
|
570
|
+
async function scaffoldDatoModels(apiToken) {
|
|
571
|
+
const { buildClient } = await import('@datocms/cma-client-node');
|
|
572
|
+
const client = buildClient({ apiToken });
|
|
573
|
+
|
|
574
|
+
// --- Block models (must be created first so we can reference them) ---
|
|
575
|
+
|
|
576
|
+
// Button block
|
|
577
|
+
console.log(' Creating Button block...');
|
|
578
|
+
const buttonBlock = await client.itemTypes.create({
|
|
579
|
+
name: 'Button',
|
|
580
|
+
api_key: 'button',
|
|
581
|
+
modular_block: true,
|
|
582
|
+
});
|
|
583
|
+
await client.fields.create(buttonBlock.id, {
|
|
584
|
+
label: 'Text',
|
|
585
|
+
field_type: 'string',
|
|
586
|
+
api_key: 'text',
|
|
587
|
+
validators: { required: {} },
|
|
588
|
+
});
|
|
589
|
+
await client.fields.create(buttonBlock.id, {
|
|
590
|
+
label: 'Variant',
|
|
591
|
+
field_type: 'string',
|
|
592
|
+
api_key: 'variant',
|
|
593
|
+
validators: { enum: { values: ['primary', 'secondary', 'outline'] } },
|
|
594
|
+
appearance: {
|
|
595
|
+
addons: [],
|
|
596
|
+
editor: 'string_select',
|
|
597
|
+
parameters: { options: [
|
|
598
|
+
{ hint: '', label: 'Primary', value: 'primary' },
|
|
599
|
+
{ hint: '', label: 'Secondary', value: 'secondary' },
|
|
600
|
+
{ hint: '', label: 'Outline', value: 'outline' },
|
|
601
|
+
]},
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
await client.fields.create(buttonBlock.id, {
|
|
605
|
+
label: 'Link',
|
|
606
|
+
field_type: 'json',
|
|
607
|
+
api_key: 'link',
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// Hero block
|
|
611
|
+
console.log(' Creating Hero block...');
|
|
612
|
+
const heroBlock = await client.itemTypes.create({
|
|
613
|
+
name: 'Hero',
|
|
614
|
+
api_key: 'hero',
|
|
615
|
+
modular_block: true,
|
|
616
|
+
});
|
|
617
|
+
await client.fields.create(heroBlock.id, {
|
|
618
|
+
label: 'Heading',
|
|
619
|
+
field_type: 'string',
|
|
620
|
+
api_key: 'heading',
|
|
621
|
+
validators: { required: {} },
|
|
622
|
+
});
|
|
623
|
+
await client.fields.create(heroBlock.id, {
|
|
624
|
+
label: 'Eyebrow',
|
|
625
|
+
field_type: 'string',
|
|
626
|
+
api_key: 'eyebrow',
|
|
627
|
+
});
|
|
628
|
+
await client.fields.create(heroBlock.id, {
|
|
629
|
+
label: 'Content',
|
|
630
|
+
field_type: 'text',
|
|
631
|
+
api_key: 'content',
|
|
632
|
+
appearance: {
|
|
633
|
+
addons: [],
|
|
634
|
+
editor: 'textarea',
|
|
635
|
+
parameters: {},
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
await client.fields.create(heroBlock.id, {
|
|
639
|
+
label: 'Layout',
|
|
640
|
+
field_type: 'string',
|
|
641
|
+
api_key: 'layout',
|
|
642
|
+
validators: { enum: { values: ['default', 'centered', 'split'] } },
|
|
643
|
+
appearance: {
|
|
644
|
+
addons: [],
|
|
645
|
+
editor: 'string_select',
|
|
646
|
+
parameters: { options: [
|
|
647
|
+
{ hint: '', label: 'Default', value: 'default' },
|
|
648
|
+
{ hint: '', label: 'Centered', value: 'centered' },
|
|
649
|
+
{ hint: '', label: 'Split', value: 'split' },
|
|
650
|
+
]},
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
await client.fields.create(heroBlock.id, {
|
|
654
|
+
label: 'Theme',
|
|
655
|
+
field_type: 'string',
|
|
656
|
+
api_key: 'theme',
|
|
657
|
+
validators: { enum: { values: ['light', 'dark'] } },
|
|
658
|
+
appearance: {
|
|
659
|
+
addons: [],
|
|
660
|
+
editor: 'string_select',
|
|
661
|
+
parameters: { options: [
|
|
662
|
+
{ hint: '', label: 'Light', value: 'light' },
|
|
663
|
+
{ hint: '', label: 'Dark', value: 'dark' },
|
|
664
|
+
]},
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
await client.fields.create(heroBlock.id, {
|
|
668
|
+
label: 'Image',
|
|
669
|
+
field_type: 'file',
|
|
670
|
+
api_key: 'image',
|
|
671
|
+
});
|
|
672
|
+
await client.fields.create(heroBlock.id, {
|
|
673
|
+
label: 'Actions',
|
|
674
|
+
field_type: 'rich_text',
|
|
675
|
+
api_key: 'actions',
|
|
676
|
+
validators: {
|
|
677
|
+
rich_text_blocks: { item_types: [buttonBlock.id] },
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// MenuItem block
|
|
682
|
+
console.log(' Creating MenuItem block...');
|
|
683
|
+
const menuItemBlock = await client.itemTypes.create({
|
|
684
|
+
name: 'Menu Item',
|
|
685
|
+
api_key: 'menu_item',
|
|
686
|
+
modular_block: true,
|
|
687
|
+
});
|
|
688
|
+
await client.fields.create(menuItemBlock.id, {
|
|
689
|
+
label: 'Heading',
|
|
690
|
+
field_type: 'string',
|
|
691
|
+
api_key: 'heading',
|
|
692
|
+
validators: { required: {} },
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// --- Regular models ---
|
|
696
|
+
|
|
697
|
+
// Page model
|
|
698
|
+
console.log(' Creating Page model...');
|
|
699
|
+
const pageModel = await client.itemTypes.create({
|
|
700
|
+
name: 'Page',
|
|
701
|
+
api_key: 'page',
|
|
702
|
+
draft_mode_active: true,
|
|
703
|
+
});
|
|
704
|
+
await client.fields.create(pageModel.id, {
|
|
705
|
+
label: 'Heading',
|
|
706
|
+
field_type: 'string',
|
|
707
|
+
api_key: 'heading',
|
|
708
|
+
validators: { required: {} },
|
|
709
|
+
});
|
|
710
|
+
await client.fields.create(pageModel.id, {
|
|
711
|
+
label: 'Description',
|
|
712
|
+
field_type: 'text',
|
|
713
|
+
api_key: 'description',
|
|
714
|
+
});
|
|
715
|
+
await client.fields.create(pageModel.id, {
|
|
716
|
+
label: 'Slug',
|
|
717
|
+
field_type: 'slug',
|
|
718
|
+
api_key: 'slug',
|
|
719
|
+
validators: {
|
|
720
|
+
required: {},
|
|
721
|
+
slug_title_field: { title_field_id: (await client.fields.list(pageModel.id)).find(f => f.api_key === 'heading').id },
|
|
722
|
+
},
|
|
723
|
+
});
|
|
724
|
+
await client.fields.create(pageModel.id, {
|
|
725
|
+
label: 'Parent Page',
|
|
726
|
+
field_type: 'link',
|
|
727
|
+
api_key: 'parent_page',
|
|
728
|
+
validators: {
|
|
729
|
+
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] },
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
await client.fields.create(pageModel.id, {
|
|
733
|
+
label: 'Body',
|
|
734
|
+
field_type: 'rich_text',
|
|
735
|
+
api_key: 'body',
|
|
736
|
+
validators: {
|
|
737
|
+
rich_text_blocks: { item_types: [heroBlock.id] },
|
|
738
|
+
},
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
// Add target link to MenuItem (now that Page exists)
|
|
742
|
+
await client.fields.create(menuItemBlock.id, {
|
|
743
|
+
label: 'Target',
|
|
744
|
+
field_type: 'link',
|
|
745
|
+
api_key: 'target',
|
|
746
|
+
validators: {
|
|
747
|
+
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] },
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// Case Study model
|
|
752
|
+
console.log(' Creating Case Study model...');
|
|
753
|
+
const caseStudyModel = await client.itemTypes.create({
|
|
754
|
+
name: 'Case Study',
|
|
755
|
+
api_key: 'case_study',
|
|
756
|
+
draft_mode_active: true,
|
|
757
|
+
});
|
|
758
|
+
await client.fields.create(caseStudyModel.id, {
|
|
759
|
+
label: 'Title',
|
|
760
|
+
field_type: 'string',
|
|
761
|
+
api_key: 'title',
|
|
762
|
+
validators: { required: {} },
|
|
763
|
+
});
|
|
764
|
+
await client.fields.create(caseStudyModel.id, {
|
|
765
|
+
label: 'Slug',
|
|
766
|
+
field_type: 'slug',
|
|
767
|
+
api_key: 'slug',
|
|
768
|
+
validators: {
|
|
769
|
+
required: {},
|
|
770
|
+
slug_title_field: { title_field_id: (await client.fields.list(caseStudyModel.id)).find(f => f.api_key === 'title').id },
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// Legal Page model
|
|
775
|
+
console.log(' Creating Legal Page model...');
|
|
776
|
+
const legalPageModel = await client.itemTypes.create({
|
|
777
|
+
name: 'Legal Page',
|
|
778
|
+
api_key: 'legal_page',
|
|
779
|
+
draft_mode_active: true,
|
|
780
|
+
});
|
|
781
|
+
await client.fields.create(legalPageModel.id, {
|
|
782
|
+
label: 'Title',
|
|
783
|
+
field_type: 'string',
|
|
784
|
+
api_key: 'title',
|
|
785
|
+
validators: { required: {} },
|
|
786
|
+
});
|
|
787
|
+
await client.fields.create(legalPageModel.id, {
|
|
788
|
+
label: 'Slug',
|
|
789
|
+
field_type: 'slug',
|
|
790
|
+
api_key: 'slug',
|
|
791
|
+
validators: {
|
|
792
|
+
required: {},
|
|
793
|
+
slug_title_field: { title_field_id: (await client.fields.list(legalPageModel.id)).find(f => f.api_key === 'title').id },
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
await client.fields.create(legalPageModel.id, {
|
|
797
|
+
label: 'Download',
|
|
798
|
+
field_type: 'file',
|
|
799
|
+
api_key: 'download',
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// Post model
|
|
803
|
+
console.log(' Creating Post model...');
|
|
804
|
+
const postModel = await client.itemTypes.create({
|
|
805
|
+
name: 'Post',
|
|
806
|
+
api_key: 'post',
|
|
807
|
+
draft_mode_active: true,
|
|
808
|
+
});
|
|
809
|
+
await client.fields.create(postModel.id, {
|
|
810
|
+
label: 'Title',
|
|
811
|
+
field_type: 'string',
|
|
812
|
+
api_key: 'title',
|
|
813
|
+
validators: { required: {} },
|
|
814
|
+
});
|
|
815
|
+
await client.fields.create(postModel.id, {
|
|
816
|
+
label: 'Slug',
|
|
817
|
+
field_type: 'slug',
|
|
818
|
+
api_key: 'slug',
|
|
819
|
+
validators: {
|
|
820
|
+
required: {},
|
|
821
|
+
slug_title_field: { title_field_id: (await client.fields.list(postModel.id)).find(f => f.api_key === 'title').id },
|
|
822
|
+
},
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// Case Studies Listing (singleton)
|
|
826
|
+
console.log(' Creating Case Studies Listing singleton...');
|
|
827
|
+
const caseStudiesListing = await client.itemTypes.create({
|
|
828
|
+
name: 'Case Studies Listing',
|
|
829
|
+
api_key: 'case_studies_listing',
|
|
830
|
+
singleton: true,
|
|
831
|
+
});
|
|
832
|
+
await client.fields.create(caseStudiesListing.id, {
|
|
833
|
+
label: 'Slug',
|
|
834
|
+
field_type: 'string',
|
|
835
|
+
api_key: 'slug',
|
|
836
|
+
validators: { required: {} },
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// Post Listing (singleton)
|
|
840
|
+
console.log(' Creating Post Listing singleton...');
|
|
841
|
+
const postListing = await client.itemTypes.create({
|
|
842
|
+
name: 'Post Listing',
|
|
843
|
+
api_key: 'post_listing',
|
|
844
|
+
singleton: true,
|
|
845
|
+
});
|
|
846
|
+
await client.fields.create(postListing.id, {
|
|
847
|
+
label: 'Slug',
|
|
848
|
+
field_type: 'string',
|
|
849
|
+
api_key: 'slug',
|
|
850
|
+
validators: { required: {} },
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
// Layout (singleton)
|
|
854
|
+
console.log(' Creating Layout singleton...');
|
|
855
|
+
const layoutModel = await client.itemTypes.create({
|
|
856
|
+
name: 'Layout',
|
|
857
|
+
api_key: 'layout',
|
|
858
|
+
singleton: true,
|
|
859
|
+
});
|
|
860
|
+
await client.fields.create(layoutModel.id, {
|
|
861
|
+
label: 'Navigation',
|
|
862
|
+
field_type: 'rich_text',
|
|
863
|
+
api_key: 'navigation',
|
|
864
|
+
validators: {
|
|
865
|
+
rich_text_blocks: { item_types: [menuItemBlock.id] },
|
|
866
|
+
},
|
|
867
|
+
});
|
|
868
|
+
await client.fields.create(layoutModel.id, {
|
|
869
|
+
label: 'Footer Navigation',
|
|
870
|
+
field_type: 'rich_text',
|
|
871
|
+
api_key: 'footer_navigation',
|
|
872
|
+
validators: {
|
|
873
|
+
rich_text_blocks: { item_types: [menuItemBlock.id] },
|
|
874
|
+
},
|
|
875
|
+
});
|
|
876
|
+
await client.fields.create(layoutModel.id, {
|
|
877
|
+
label: 'Copyright',
|
|
878
|
+
field_type: 'string',
|
|
879
|
+
api_key: 'copyright',
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
console.log('');
|
|
883
|
+
console.log(' Models created: Page, Case Study, Legal Page, Post');
|
|
884
|
+
console.log(' Singletons created: Layout, Case Studies Listing, Post Listing');
|
|
885
|
+
console.log(' Blocks created: Hero, Button, Menu Item');
|
|
886
|
+
}
|
|
887
|
+
|
|
476
888
|
function generateReadme(projectName, cms) {
|
|
477
889
|
const title = projectName || 'Project Title';
|
|
478
890
|
|
package/package.json
CHANGED