diff --git a/e2e-projects/next/package.json b/e2e-projects/next/package.json index f8c2ff2f91..621570a244 100644 --- a/e2e-projects/next/package.json +++ b/e2e-projects/next/package.json @@ -1,6 +1,6 @@ { "name": "cimsirp", - "version": "1.11.0", + "version": "1.11.1-dev-next-release.7", "private": true, "scripts": { "dev": "next dev", diff --git a/e2e-projects/sveltekit/package.json b/e2e-projects/sveltekit/package.json index 338d38e493..177a4cc75a 100644 --- a/e2e-projects/sveltekit/package.json +++ b/e2e-projects/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "sveltekit", - "version": "1.11.0", + "version": "1.11.1-dev-next-release.7", "private": true, "scripts": { "dev": "vite dev", diff --git a/package.json b/package.json index f5efafefb0..b4fba75e8d 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "prettier:fix": "prettier --write .", "prettier:check": "prettier --check .", "test": "yarn workspaces foreach --parallel --topological-dev --verbose run test", - "test:e2e": "yarn cypress-setup && start-server-and-test 'npm run dev --prefix e2e-projects/cypress-next-app' http://localhost:3000 'SM_ENV=development ENABLE_SENTRY=false npm run slicemachine --prefix e2e-projects/cypress-next-app' http://localhost:9999 'cypress run'", - "test:e2e:dev": "yarn cypress-setup && start-server-and-test 'npm run dev --prefix e2e-projects/cypress-next-app' http://localhost:3000 'SM_ENV=staging ENABLE_SENTRY=false npm run slicemachine --prefix e2e-projects/cypress-next-app' http://localhost:9999 'cypress open'", + "test:e2e": "yarn cypress-setup && start-server-and-test 'npm run dev --prefix e2e-projects/cypress-next-app' http://localhost:3000 'SM_ENV=development VITE_ENABLE_SENTRY=false npm run slicemachine --prefix e2e-projects/cypress-next-app' http://localhost:9999 'cypress run'", + "test:e2e:dev": "yarn cypress-setup && start-server-and-test 'npm run dev --prefix e2e-projects/cypress-next-app' http://localhost:3000 'SM_ENV=staging VITE_ENABLE_SENTRY=false npm run slicemachine --prefix e2e-projects/cypress-next-app' http://localhost:9999 'cypress open'", "cy:open": "cypress open", "bump:interactive": "lerna version prerelease --preid alpha --no-push --exact", "bump:alpha": "lerna version prerelease --preid $npm_config_preid --no-changelog --exact --yes", diff --git a/packages/adapter-next/package.json b/packages/adapter-next/package.json index 9f33410247..8429a5716e 100644 --- a/packages/adapter-next/package.json +++ b/packages/adapter-next/package.json @@ -1,6 +1,6 @@ { "name": "@slicemachine/adapter-next", - "version": "0.3.13", + "version": "0.3.14-dev-next-release.7", "description": "Slice Machine adapter for Next.js.", "keywords": [ "typescript", diff --git a/packages/adapter-next/public/CallToAction/screenshot-alignLeft.png b/packages/adapter-next/public/CallToAction/screenshot-alignLeft.png new file mode 100644 index 0000000000..18dfd18966 Binary files /dev/null and b/packages/adapter-next/public/CallToAction/screenshot-alignLeft.png differ diff --git a/packages/adapter-next/public/CallToAction/screenshot-default.png b/packages/adapter-next/public/CallToAction/screenshot-default.png new file mode 100644 index 0000000000..e5c139b610 Binary files /dev/null and b/packages/adapter-next/public/CallToAction/screenshot-default.png differ diff --git a/packages/adapter-next/src/hooks/slice-create.ts b/packages/adapter-next/src/hooks/slice-create.ts index d0e3f27fce..3012d09d62 100644 --- a/packages/adapter-next/src/hooks/slice-create.ts +++ b/packages/adapter-next/src/hooks/slice-create.ts @@ -39,7 +39,9 @@ const createComponentFile = async ({ options, }); - if (isTypeScriptProject) { + if (data.componentContents) { + contents = data.componentContents; + } else if (isTypeScriptProject) { contents = stripIndent` import { Content } from "@prismicio/client"; import { SliceComponentProps } from "@prismicio/react"; diff --git a/packages/adapter-next/src/hooks/sliceTemplateLibrary-read.ts b/packages/adapter-next/src/hooks/sliceTemplateLibrary-read.ts new file mode 100644 index 0000000000..a87c0c0cd8 --- /dev/null +++ b/packages/adapter-next/src/hooks/sliceTemplateLibrary-read.ts @@ -0,0 +1,55 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; +import type { SliceTemplateLibraryReadHook } from "@slicemachine/plugin-kit"; + +import { checkIsTypeScriptProject } from "../lib/checkIsTypeScriptProject"; + +import * as CallToAction from "../sliceTemplates/CallToAction"; +import type { PluginOptions } from "../types"; + +const initialTemplates = [CallToAction]; + +export const sliceTemplateLibraryRead: SliceTemplateLibraryReadHook< + PluginOptions +> = async ({ templateIDs }, { helpers, options }) => { + const isTypeScriptProject = await checkIsTypeScriptProject({ + helpers, + options, + }); + const templates = + templateIDs && templateIDs.length + ? initialTemplates.filter((t) => templateIDs?.includes(t.model.id)) + : initialTemplates; + + const templatesPromises = templates.map(async (t) => { + const { mocks, model, createComponentContents, screenshotPaths } = t; + + const screenshotEntries = Object.entries(screenshotPaths); + const screenshotPromises = screenshotEntries.map(([key, filePath]) => { + return fs + .readFile( + fileURLToPath(new URL(path.join("..", filePath), import.meta.url)), + ) + .then((data) => [key, data]); + }); + const readScreenshots = await Promise.all(screenshotPromises); + const screenshots = Object.fromEntries(readScreenshots); + + return { + mocks, + model, + createComponentContents: (model: SharedSlice) => + createComponentContents(model, isTypeScriptProject), + screenshots, + }; + }); + + const resolvedTemplates = await Promise.all(templatesPromises); + + return { + templates: resolvedTemplates, + }; +}; diff --git a/packages/adapter-next/src/plugin.ts b/packages/adapter-next/src/plugin.ts index 8e6df3b3ec..7604c0d4dc 100644 --- a/packages/adapter-next/src/plugin.ts +++ b/packages/adapter-next/src/plugin.ts @@ -29,6 +29,7 @@ import { documentationRead } from "./hooks/documentation-read"; import { projectInit } from "./hooks/project-init"; import { sliceCreate } from "./hooks/slice-create"; import { sliceSimulatorSetupRead } from "./hooks/sliceSimulator-setup-read"; +import { sliceTemplateLibraryRead } from "./hooks/sliceTemplateLibrary-read"; import { snippetRead } from "./hooks/snippet-read"; export const plugin = defineSliceMachinePlugin({ @@ -155,6 +156,12 @@ export const plugin = defineSliceMachinePlugin({ }); }); + //////////////////////////////////////////////////////////////// + // slice-template-library:* + //////////////////////////////////////////////////////////////// + + hook("slice-template-library:read", sliceTemplateLibraryRead); + //////////////////////////////////////////////////////////////// // custom-type:* //////////////////////////////////////////////////////////////// diff --git a/packages/adapter-next/src/sliceTemplates/CallToAction/index.ts b/packages/adapter-next/src/sliceTemplates/CallToAction/index.ts new file mode 100644 index 0000000000..6235dd5dc7 --- /dev/null +++ b/packages/adapter-next/src/sliceTemplates/CallToAction/index.ts @@ -0,0 +1,561 @@ +import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes"; +import { SharedSliceContent } from "@prismicio/types-internal/lib/content"; + +import { pascalCase } from "../../lib/pascalCase"; + +import { stripIndent } from "common-tags"; + +export const mocks: SharedSliceContent[] = [ + { + __TYPE__: "SharedSliceContent", + variation: "default", + primary: { + title: { + __TYPE__: "StructuredTextContent", + value: [ + { + type: "heading1", + content: { + text: "Collector Slices kit", + spans: [], + }, + direction: "ltr", + }, + ], + }, + paragraph: { + __TYPE__: "StructuredTextContent", + value: [ + { + type: "paragraph", + content: { + text: "It’s very easy to create stylish and beautiful prototypes for your future projects, both graphical and dynamic.", + spans: [], + }, + direction: "ltr", + }, + ], + }, + buttonLink: { + __TYPE__: "LinkContent", + value: { + __TYPE__: "ExternalLink", + url: "https://twitter.com/prismicio", + }, + }, + buttonlabel: { + __TYPE__: "FieldContent", + type: "Text", + value: "Click here", + }, + image: { + origin: { + id: "9aOswReDKPo", + url: "https://images.unsplash.com/photo-1523049673857-eb18f1d7b578?crop=entropy&cs=srgb&fm=jpg&ixid=M3wzMzc0NjN8MHwxfHNlYXJjaHwxOXx8ZnJ1aXR8ZW58MHx8fHwxNjkzMjUxNTMxfDA&ixlib=rb-4.0.3&q=85", + width: 8040, + height: 6024, + }, + url: "https://images.unsplash.com/photo-1523049673857-eb18f1d7b578?crop=entropy&cs=srgb&fm=jpg&ixid=M3wzMzc0NjN8MHwxfHNlYXJjaHwxOXx8ZnJ1aXR8ZW58MHx8fHwxNjkzMjUxNTMxfDA&ixlib=rb-4.0.3&q=85", + width: 8040, + height: 6024, + edit: { + background: "transparent", + zoom: 1, + crop: { + x: 0, + y: 0, + }, + }, + credits: null, + alt: "Example Image", + __TYPE__: "ImageContent", + thumbnails: {}, + }, + buttonLabel: { + __TYPE__: "FieldContent", + type: "Text", + value: "Learn more", + }, + }, + items: [], + }, + { + __TYPE__: "SharedSliceContent", + variation: "alignLeft", + primary: { + image: { + origin: { + id: "euqiHwS38Rw", + url: "https://images.unsplash.com/photo-1595475207225-428b62bda831?crop=entropy&cs=srgb&fm=jpg&ixid=M3wzMzc0NjN8MHwxfHNlYXJjaHwyMHx8ZnJ1aXR8ZW58MHx8fHwxNjkzMjUxNTMxfDA&ixlib=rb-4.0.3&q=85", + width: 2500, + height: 2500, + }, + url: "https://images.unsplash.com/photo-1595475207225-428b62bda831?crop=entropy&cs=srgb&fm=jpg&ixid=M3wzMzc0NjN8MHwxfHNlYXJjaHwyMHx8ZnJ1aXR8ZW58MHx8fHwxNjkzMjUxNTMxfDA&ixlib=rb-4.0.3&q=85", + width: 2500, + height: 2500, + edit: { + background: "transparent", + zoom: 1, + crop: { + x: 0, + y: 0, + }, + }, + credits: null, + alt: "Example Image", + __TYPE__: "ImageContent", + thumbnails: {}, + }, + title: { + __TYPE__: "StructuredTextContent", + value: [ + { + type: "heading1", + content: { + text: "Collector Slices kit", + spans: [], + }, + direction: "ltr", + }, + ], + }, + paragraph: { + __TYPE__: "StructuredTextContent", + value: [ + { + type: "paragraph", + content: { + text: "It’s very easy to create stylish and beautiful prototypes for your future projects, both graphical and dynamic.", + spans: [], + }, + direction: "ltr", + }, + ], + }, + buttonLink: { + __TYPE__: "LinkContent", + value: { + __TYPE__: "ExternalLink", + url: "https://prismic.io", + }, + }, + buttonLabel: { + __TYPE__: "FieldContent", + type: "Text", + value: "Learn more!", + }, + }, + items: [ + { + __TYPE__: "GroupItemContent", + value: [], + }, + ], + }, +]; + +export const createComponentContents = ( + model: SharedSlice, + isTypeScriptProject: boolean, +): string => { + const pascalName = pascalCase(model.name); + + if (isTypeScriptProject) { + return stripIndent` + import { Content, isFilled } from "@prismicio/client"; + import { PrismicNextLink, PrismicNextImage } from "@prismicio/next"; + import { + PrismicRichText, + PrismicText, + SliceComponentProps + } from "@prismicio/react"; + + /** + * Props for \`${pascalName}\`. + */ + export type ${pascalName}Props = SliceComponentProps; + + /** + * Component for "${model.name}" Slices. + */ + const ${pascalName} = ({ slice }: ${pascalName}Props): JSX.Element => { + const alignment = slice.variation === "alignLeft" ? "left" : "center"; + + return ( +
+
+ {isFilled.image(slice.primary.image) && ( + + )} +
+ {isFilled.richText(slice.primary.title) && ( +

+ +

+ )} + {isFilled.richText(slice.primary.paragraph) && ( +
+ +
+ )} +
+ {isFilled.link(slice.primary.buttonLink) && ( + + {slice.primary.buttonLabel || "Learn more…"} + + )} +
+ +
+ ); + }; + + export default ${pascalName}; + `; + } + + return stripIndent` + import { isFilled } from "@prismicio/client"; + import { PrismicNextLink, PrismicNextImage } from "@prismicio/next"; + import { + PrismicRichText, + PrismicText, + SliceComponentProps + } from "@prismicio/react"; + + /** + * @typedef {import("@prismicio/client").Content.${pascalName}Slice} ${pascalName}Slice + * @typedef {import("@prismicio/react").SliceComponentProps<${pascalName}Slice>} ${pascalName}Props + * @param {${pascalName}Props} + */ + const ${pascalName} = ({ slice }) => { + const alignment = slice.variation === "alignLeft" ? "left" : "center"; + + return ( +
+
+ {isFilled.image(slice.primary.image) && ( + + )} +
+ {isFilled.richText(slice.primary.title) && ( +

+ +

+ )} + {isFilled.richText(slice.primary.paragraph) && ( +
+ +
+ )} +
+ {isFilled.link(slice.primary.buttonLink) && ( + + {slice.primary.buttonLabel || "Learn more…"} + + )} +
+ +
+ ); + }; + + export default ${pascalName}; + `; +}; + +export const model: SharedSlice = { + id: "call_to_action", + type: "SharedSlice", + name: "CallToAction", + description: "CallToAction", + variations: [ + { + id: "default", + name: "Default", + docURL: "...", + version: "initial", + description: "Default", + imageUrl: "", + primary: { + image: { + type: "Image", + config: { label: "Image", constraint: {}, thumbnails: [] }, + }, + title: { + type: "StructuredText", + config: { + label: "title", + placeholder: "", + allowTargetBlank: true, + single: "heading1,heading2,heading3,heading4,heading5,heading6", + }, + }, + paragraph: { + type: "StructuredText", + config: { + label: "paragraph", + placeholder: "", + allowTargetBlank: true, + single: + "paragraph,preformatted,strong,em,hyperlink,image,embed,list-item,o-list-item,rtl", + }, + }, + buttonLink: { + type: "Link", + config: { + label: "buttonLink", + placeholder: "Redirect URL for CTA button", + allowTargetBlank: true, + select: null, + }, + }, + buttonLabel: { + type: "Text", + config: { + label: "buttonLabel", + placeholder: "Label for CTA button", + }, + }, + }, + items: {}, + }, + { + id: "alignLeft", + name: "AlignLeft", + docURL: "...", + version: "initial", + description: "Default", + imageUrl: "", + primary: { + image: { + type: "Image", + config: { label: "Image", constraint: {}, thumbnails: [] }, + }, + title: { + type: "StructuredText", + config: { + label: "title", + placeholder: "", + allowTargetBlank: true, + single: "heading1,heading2,heading3,heading4,heading5,heading6", + }, + }, + paragraph: { + type: "StructuredText", + config: { + label: "paragraph", + placeholder: "", + allowTargetBlank: true, + single: + "paragraph,preformatted,strong,em,hyperlink,image,embed,list-item,o-list-item,rtl", + }, + }, + buttonLink: { + type: "Link", + config: { + label: "buttonLink", + placeholder: "Redirect URL for CTA button", + allowTargetBlank: true, + select: null, + }, + }, + buttonLabel: { + type: "Text", + config: { + label: "buttonLabel", + placeholder: "Label for CTA button", + }, + }, + }, + items: {}, + }, + ], +}; + +export const screenshotPaths = { + default: "CallToAction/screenshot-default.png", + alignLeft: "CallToAction/screenshot-alignLeft.png", +}; diff --git a/packages/adapter-next/test/plugin-slice-create.test.ts b/packages/adapter-next/test/plugin-slice-create.test.ts index 8e2cbb3644..b74a2f6c0b 100644 --- a/packages/adapter-next/test/plugin-slice-create.test.ts +++ b/packages/adapter-next/test/plugin-slice-create.test.ts @@ -392,3 +392,26 @@ testGlobalContentTypes({ await pluginRunner.callHook("slice:create", { libraryID: "slices", model }); }, }); + +test("component file contains given contents instead of default one", async (ctx) => { + await ctx.pluginRunner.callHook("slice:create", { + libraryID: "slices", + model, + componentContents: ` + export default function TestSliceCreate() { + return ( +
Custom contents
+ ); + } + `, + }); + + const componentContents = await fs.readFile( + path.join(ctx.project.root, "slices", "QuxQuux", "index.js"), + "utf8", + ); + + expect(componentContents).toContain( + "export default function TestSliceCreate()", + ); +}); diff --git a/packages/adapter-nuxt/package.json b/packages/adapter-nuxt/package.json index 58e0bb858e..c05588bc7b 100644 --- a/packages/adapter-nuxt/package.json +++ b/packages/adapter-nuxt/package.json @@ -1,6 +1,6 @@ { "name": "@slicemachine/adapter-nuxt", - "version": "0.3.13", + "version": "0.3.14-dev-next-release.7", "description": "Slice Machine adapter for Nuxt 3.", "keywords": [ "typescript", diff --git a/packages/adapter-nuxt/src/hooks/project-init.ts b/packages/adapter-nuxt/src/hooks/project-init.ts index 7ad6df1790..90bd531ae7 100644 --- a/packages/adapter-nuxt/src/hooks/project-init.ts +++ b/packages/adapter-nuxt/src/hooks/project-init.ts @@ -6,6 +6,8 @@ import type { } from "@slicemachine/plugin-kit"; import { checkHasProjectFile, + deleteProjectFile, + readProjectFile, writeProjectFile, } from "@slicemachine/plugin-kit/fs"; import { stripIndent } from "common-tags"; @@ -154,6 +156,60 @@ const createSliceSimulatorPage = async ({ }); }; +const moveOrDeleteAppVue = async ({ + helpers, + options, +}: CreateSliceSimulatorPageArgs) => { + const srcDirectoryExists = await checkHasProjectFile({ + filename: "src", + helpers, + }); + + const filenameAppVue = path.join(srcDirectoryExists ? "src" : "", "app.vue"); + + // If there's not `app.vue`, there's nothing to do. + if (!(await checkHasProjectFile({ filename: filenameAppVue, helpers }))) { + return; + } + + const filecontentAppVue = await readProjectFile({ + filename: filenameAppVue, + helpers, + encoding: "utf-8", + }); + + // We check for app.vue to contain Nuxt default welcome component to determine if we need to consider it as the default one or not. + if (!filecontentAppVue.includes(" = async ( installDependencies({ installDependencies: _installDependencies }), configurePrismicModule(context), createSliceSimulatorPage(context), + moveOrDeleteAppVue(context), modifySliceMachineConfig(context), ]), ); diff --git a/packages/adapter-nuxt/src/hooks/slice-create.ts b/packages/adapter-nuxt/src/hooks/slice-create.ts index 3c8edbd3f7..3694d2be86 100644 --- a/packages/adapter-nuxt/src/hooks/slice-create.ts +++ b/packages/adapter-nuxt/src/hooks/slice-create.ts @@ -36,7 +36,9 @@ const createComponentFile = async ({ options, }); - if (isTypeScriptProject) { + if (data.componentContents) { + contents = data.componentContents; + } else if (isTypeScriptProject) { contents = stripIndent` - `; + export default { + // The array passed to \`getSliceComponentProps\` is purely optional. + // Consider it as a visual hint for you when templating your slice. + props: getSliceComponentProps(["slice", "index", "slices", "context"]), + }; + + `; await writeSliceFile({ libraryID: data.libraryID, diff --git a/packages/adapter-nuxt2/test/plugin-slice-create.test.ts b/packages/adapter-nuxt2/test/plugin-slice-create.test.ts index 273a04e122..3ab04d151d 100644 --- a/packages/adapter-nuxt2/test/plugin-slice-create.test.ts +++ b/packages/adapter-nuxt2/test/plugin-slice-create.test.ts @@ -240,3 +240,22 @@ testGlobalContentTypes({ await pluginRunner.callHook("slice:create", { libraryID: "slices", model }); }, }); + +test("component file contains given contents instead of default one", async (ctx) => { + await ctx.pluginRunner.callHook("slice:create", { + libraryID: "slices", + model, + componentContents: ` + + `, + }); + + const componentContents = await fs.readFile( + path.join(ctx.project.root, "slices", "QuxQuux", "index.vue"), + "utf8", + ); + + expect(componentContents).toContain("
TestSliceCreate
"); +}); diff --git a/packages/adapter-sveltekit/package.json b/packages/adapter-sveltekit/package.json index 1806a8a591..586695ee86 100644 --- a/packages/adapter-sveltekit/package.json +++ b/packages/adapter-sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@slicemachine/adapter-sveltekit", - "version": "0.3.13", + "version": "0.3.14-dev-next-release.7", "description": "Slice Machine adapter for SvelteKit.", "keywords": [ "typescript", diff --git a/packages/adapter-sveltekit/src/hooks/slice-create.ts b/packages/adapter-sveltekit/src/hooks/slice-create.ts index c545ee8179..84c83f7f81 100644 --- a/packages/adapter-sveltekit/src/hooks/slice-create.ts +++ b/packages/adapter-sveltekit/src/hooks/slice-create.ts @@ -36,7 +36,9 @@ const createComponentFile = async ({ options, }); - if (isTypeScriptProject) { + if (data.componentContents) { + contents = data.componentContents; + } else if (isTypeScriptProject) { contents = source`