@slicemachine/adapter-next 0.0.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.
@@ -0,0 +1,50 @@
1
+ import type {
2
+ SliceDeleteHook,
3
+ SliceDeleteHookData,
4
+ SliceMachineContext,
5
+ } from "@slicemachine/plugin-kit";
6
+ import * as fs from "node:fs/promises";
7
+
8
+ import { buildSliceLibraryIndexFileContents } from "../lib/buildSliceLibraryIndexFileContents";
9
+ import { pascalCase } from "../lib/pascalCase";
10
+
11
+ import type { PluginOptions } from "../types";
12
+
13
+ type Args = {
14
+ data: SliceDeleteHookData;
15
+ } & SliceMachineContext<PluginOptions>;
16
+
17
+ const deleteSliceDir = async ({ data, helpers }: Args) => {
18
+ const dir = helpers.joinPathFromRoot(
19
+ data.libraryID,
20
+ pascalCase(data.model.id),
21
+ );
22
+
23
+ await fs.rm(dir, { recursive: true });
24
+ };
25
+
26
+ const updateSliceLibraryIndexFile = async ({
27
+ data,
28
+ actions,
29
+ helpers,
30
+ project,
31
+ options,
32
+ }: Args) => {
33
+ const { filePath, contents } = await buildSliceLibraryIndexFileContents({
34
+ libraryID: data.libraryID,
35
+ actions,
36
+ helpers,
37
+ project,
38
+ options,
39
+ });
40
+
41
+ await fs.writeFile(filePath, contents);
42
+ };
43
+
44
+ export const sliceDelete: SliceDeleteHook<PluginOptions> = async (
45
+ data,
46
+ context,
47
+ ) => {
48
+ await deleteSliceDir({ data, ...context });
49
+ await updateSliceLibraryIndexFile({ data, ...context });
50
+ };
@@ -0,0 +1,51 @@
1
+ import type { SliceLibraryReadHook } from "@slicemachine/plugin-kit";
2
+ import * as prismicT from "@prismicio/types";
3
+ import * as fs from "node:fs/promises";
4
+ import * as path from "node:path";
5
+
6
+ import { readJSONFile } from "../lib/readJSONFile";
7
+
8
+ import type { PluginOptions } from "../types";
9
+
10
+ const isSharedSliceModel = (
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ input: any,
13
+ ): input is prismicT.SharedSliceModel => {
14
+ return (
15
+ typeof input === "object" &&
16
+ input !== null &&
17
+ "type" in input &&
18
+ input.type === prismicT.CustomTypeModelSliceType.SharedSlice
19
+ );
20
+ };
21
+
22
+ export const sliceLibraryRead: SliceLibraryReadHook<PluginOptions> = async (
23
+ data,
24
+ { helpers },
25
+ ) => {
26
+ const dirPath = helpers.joinPathFromRoot(data.libraryID);
27
+
28
+ const childDirs = await fs.readdir(dirPath);
29
+
30
+ const sliceIDs: string[] = [];
31
+ await Promise.all(
32
+ childDirs.map(async (childDir) => {
33
+ const modelPath = path.join(dirPath, childDir, "model.json");
34
+
35
+ try {
36
+ const modelContents = await readJSONFile(modelPath);
37
+
38
+ if (isSharedSliceModel(modelContents)) {
39
+ sliceIDs.push(modelContents.id);
40
+ }
41
+ } catch {
42
+ // noop
43
+ }
44
+ }),
45
+ );
46
+
47
+ return {
48
+ id: data.libraryID,
49
+ sliceIDs: sliceIDs.sort(),
50
+ };
51
+ };
@@ -0,0 +1,20 @@
1
+ import type { SliceReadHook } from "@slicemachine/plugin-kit";
2
+ import * as prismicT from "@prismicio/types";
3
+
4
+ import { readJSONFile } from "../lib/readJSONFile";
5
+ import { pascalCase } from "../lib/pascalCase";
6
+
7
+ import type { PluginOptions } from "../types";
8
+
9
+ export const sliceRead: SliceReadHook<PluginOptions> = async (
10
+ data,
11
+ { helpers },
12
+ ) => {
13
+ const filePath = helpers.joinPathFromRoot(
14
+ data.libraryID,
15
+ pascalCase(data.sliceID),
16
+ "model.json",
17
+ );
18
+
19
+ return await readJSONFile<prismicT.SharedSliceModel>(filePath);
20
+ };
@@ -0,0 +1,58 @@
1
+ import type {
2
+ SliceMachineContext,
3
+ SliceUpdateHook,
4
+ SliceUpdateHookData,
5
+ } from "@slicemachine/plugin-kit";
6
+ import { generateTypes } from "prismic-ts-codegen";
7
+ import * as fs from "node:fs/promises";
8
+ import * as path from "node:path";
9
+
10
+ import { pascalCase } from "../lib/pascalCase";
11
+
12
+ import type { PluginOptions } from "../types";
13
+
14
+ type Args = {
15
+ dir: string;
16
+ data: SliceUpdateHookData;
17
+ } & SliceMachineContext<PluginOptions>;
18
+
19
+ const updateModelFile = async ({ dir, data, helpers, options }: Args) => {
20
+ const filePath = path.join(dir, "model.json");
21
+
22
+ let contents = JSON.stringify(data.model);
23
+
24
+ if (options.format) {
25
+ contents = await helpers.format(contents, filePath);
26
+ }
27
+
28
+ await fs.writeFile(filePath, contents);
29
+ };
30
+
31
+ const updateTypesFile = async ({ dir, data, helpers, options }: Args) => {
32
+ const filePath = path.join(dir, "types.ts");
33
+
34
+ let contents = generateTypes({
35
+ sharedSliceModels: [data.model],
36
+ });
37
+
38
+ if (options.format) {
39
+ contents = await helpers.format(contents, filePath);
40
+ }
41
+
42
+ await fs.writeFile(filePath, contents);
43
+ };
44
+
45
+ export const sliceUpdate: SliceUpdateHook<PluginOptions> = async (
46
+ data,
47
+ context,
48
+ ) => {
49
+ const dir = context.helpers.joinPathFromRoot(
50
+ data.libraryID,
51
+ pascalCase(data.model.id),
52
+ );
53
+
54
+ await Promise.allSettled([
55
+ updateModelFile({ dir, data, ...context }),
56
+ updateTypesFile({ dir, data, ...context }),
57
+ ]);
58
+ };
@@ -0,0 +1,244 @@
1
+ import type {
2
+ SliceMachineContext,
3
+ SliceSimulatorSetupReadHook,
4
+ SliceSimulatorSetupStep,
5
+ } from "@slicemachine/plugin-kit";
6
+ import { SliceSimulatorSetupStepValidationMessageType } from "@slicemachine/plugin-kit";
7
+ import { stripIndent } from "common-tags";
8
+ import { createRequire } from "node:module";
9
+ import * as http from "node:http";
10
+
11
+ import { getJSOrTSXFileExtension } from "../lib/getJSOrTSXFileExtension";
12
+
13
+ import type { PluginOptions } from "../types";
14
+
15
+ const REQUIRED_DEPENDENCIES = [
16
+ "@prismicio/react",
17
+ "@prismicio/slice-simulator-react",
18
+ "@prismicio/client@latest",
19
+ "@prismicio/helpers",
20
+ ];
21
+
22
+ type Args = SliceMachineContext<PluginOptions>;
23
+
24
+ const createStep1 = async ({
25
+ project,
26
+ }: Args): Promise<SliceSimulatorSetupStep> => {
27
+ const require = createRequire(project.root);
28
+
29
+ return {
30
+ title: "Install packages",
31
+ body: stripIndent`
32
+ The simulator requires extra dependencies. Run the following command to install them.
33
+
34
+ ~~~sh
35
+ npm install --save @prismicio/react @prismicio/slice-simulator-react @prismicio/client@latest @prismicio/helpers
36
+ ~~~
37
+ `,
38
+ validate: async () => {
39
+ const missingDependencies: string[] = [];
40
+
41
+ for (const dependency of REQUIRED_DEPENDENCIES) {
42
+ try {
43
+ // `require.resolve()` is preferred
44
+ // over `import()` because we don't
45
+ // want to load the module. Loading a
46
+ // module could introduce side-effects.
47
+ require.resolve(dependency);
48
+ } catch {
49
+ missingDependencies.push(dependency);
50
+ }
51
+ }
52
+
53
+ if (missingDependencies.length >= REQUIRED_DEPENDENCIES.length) {
54
+ return {
55
+ type: SliceSimulatorSetupStepValidationMessageType.Error,
56
+ title: "Missing all dependencies",
57
+ message: stripIndent`
58
+ Install the required dependencies to continue.
59
+ `,
60
+ };
61
+ }
62
+
63
+ if (missingDependencies.length > 0) {
64
+ const formattedMissingDependencies = missingDependencies
65
+ .map((missingDependency) => `\`${missingDependency}\``)
66
+ .join(", ");
67
+
68
+ return {
69
+ type: SliceSimulatorSetupStepValidationMessageType.Warning,
70
+ title: "Missing some dependencies",
71
+ message: stripIndent`
72
+ The following dependencies are missing: ${formattedMissingDependencies}
73
+ `,
74
+ };
75
+ }
76
+ },
77
+ };
78
+ };
79
+
80
+ const createStep2 = async ({
81
+ helpers,
82
+ options,
83
+ }: Args): Promise<SliceSimulatorSetupStep> => {
84
+ const fileName = `slice-simulator.${getJSOrTSXFileExtension}`;
85
+ const filePath = helpers.joinPathFromRoot("pages", fileName);
86
+
87
+ let fileContents: string;
88
+
89
+ if (options.typescript) {
90
+ fileContents = stripIndent`
91
+ import { GetStaticProps } from "next/types";
92
+ import { SliceSimulator } from "@prismicio/slice-simulator-react";
93
+ import { SliceZone } from "@prismicio/react";
94
+
95
+ import state from "../.slicemachine/libraries-state.json";
96
+ import { components } from "../slices";
97
+
98
+ const SliceSimulatorPage = () => {
99
+ return (
100
+ <SliceSimulator
101
+ sliceZone={(props) => <SliceZone {...props} components={components} />}
102
+ state={state}
103
+ />
104
+ );
105
+ };
106
+
107
+ export default SliceSimulatorPage;
108
+
109
+ export const getStaticProps: GetStaticProps = () => {
110
+ return {
111
+ // Exclude this page from production builds.
112
+ notFound: process.env.NODE_ENV === "production",
113
+ };
114
+ };
115
+ `;
116
+ } else {
117
+ fileContents = stripIndent`
118
+ import { SliceSimulator } from "@prismicio/slice-simulator-react";
119
+ import { SliceZone } from "@prismicio/react";
120
+
121
+ import state from "../.slicemachine/libraries-state.json";
122
+ import { components } from "../slices";
123
+
124
+ const SliceSimulatorPage = () => {
125
+ return (
126
+ <SliceSimulator
127
+ sliceZone={(props) => <SliceZone {...props} components={components} />}
128
+ state={state}
129
+ />
130
+ );
131
+ };
132
+
133
+ export default SliceSimulatorPage;
134
+
135
+ export const getStaticProps= () => {
136
+ return {
137
+ // Exclude this page from production builds.
138
+ notFound: process.env.NODE_ENV === "production",
139
+ };
140
+ };
141
+ `;
142
+ }
143
+
144
+ fileContents = await helpers.format(fileContents, filePath);
145
+
146
+ return {
147
+ title: "Create a page for the simulator",
148
+ body: stripIndent`
149
+ In your \`pages\` directory, create a file called \`${fileName}\` and add the following code. This route will be used to simulate and develop your components.
150
+
151
+ ~~~tsx
152
+ ${fileContents}
153
+ ~~~
154
+ `,
155
+ };
156
+ };
157
+
158
+ const createStep3 = async ({
159
+ helpers,
160
+ }: Args): Promise<SliceSimulatorSetupStep> => {
161
+ const filePath = helpers.joinPathFromRoot("sm.json");
162
+ const fileContents = await helpers.format(
163
+ `
164
+ {
165
+ "localSliceSimulatorURL": "http://localhost:3000/slice-simulator"
166
+ }
167
+ `,
168
+ filePath,
169
+ );
170
+
171
+ return {
172
+ title: "Update `sm.json`",
173
+ body: stripIndent`
174
+ Update your \`sm.json\` file with a \`localSliceSimulatorURL\` property pointing to your \`slice-simulator\` page.
175
+
176
+ ~~~json
177
+ ${fileContents}
178
+ ~~~
179
+ `,
180
+ validate: async () => {
181
+ const project = await helpers.getProject();
182
+
183
+ if (!("localSliceSimulatorURL" in project.config)) {
184
+ return {
185
+ type: SliceSimulatorSetupStepValidationMessageType.Error,
186
+ title: "Missing `localSliceSimulatorURL` property",
187
+ message: stripIndent`
188
+ A \`localSliceSimulatorURL\` property was not found in your \`sm.json\` file.
189
+ `,
190
+ };
191
+ }
192
+
193
+ // Test if the URL is valid.
194
+ try {
195
+ if (project.config.localSliceSimulatorURL) {
196
+ new URL(project.config.localSliceSimulatorURL);
197
+ } else {
198
+ throw new Error("Undefined Slice Simulator URL");
199
+ }
200
+ } catch {
201
+ return {
202
+ type: SliceSimulatorSetupStepValidationMessageType.Warning,
203
+ title: "An invalid URL was provided",
204
+ message: stripIndent`
205
+ The \`localSliceSimulatorURL\` property should be of the shape \`http://localhost:PORT/PATH\`. See the codeblock for an example.
206
+ `,
207
+ };
208
+ }
209
+
210
+ // Check if the URL is accessible.
211
+ const ok = await new Promise<boolean>((resolve) => {
212
+ if (project.config.localSliceSimulatorURL) {
213
+ http.get(project.config.localSliceSimulatorURL, (res) => {
214
+ if (res.statusCode) {
215
+ resolve(res.statusCode >= 200 && res.statusCode < 300);
216
+ }
217
+ });
218
+ }
219
+
220
+ resolve(false);
221
+ });
222
+
223
+ if (!ok) {
224
+ return {
225
+ type: SliceSimulatorSetupStepValidationMessageType.Warning,
226
+ title: "Unable to connect to simulator page",
227
+ message: stripIndent`
228
+ Check that the \`localSliceSimulatorURL\` property in \`sm.json\` is correct and try again. See the [troubleshooting page](https://prismic.io/docs/technologies/setup-slice-simulator-nextjs) for more details.
229
+ `,
230
+ };
231
+ }
232
+ },
233
+ };
234
+ };
235
+
236
+ export const sliceSimulatorSetupRead: SliceSimulatorSetupReadHook<
237
+ PluginOptions
238
+ > = async (_data, context) => {
239
+ return Promise.all([
240
+ createStep1(context),
241
+ createStep2(context),
242
+ createStep3(context),
243
+ ]);
244
+ };
@@ -0,0 +1,115 @@
1
+ import type { SnippetReadHook } from "@slicemachine/plugin-kit";
2
+ import * as prismicT from "@prismicio/types";
3
+ import { stripIndent } from "common-tags";
4
+ import type { Config as PrettierConfig } from "prettier";
5
+
6
+ import type { PluginOptions } from "../types";
7
+
8
+ const prettierOptions: PrettierConfig = { parser: "typescript" };
9
+
10
+ const dotPath = (segments: string[]): string => {
11
+ return segments.join(".");
12
+ };
13
+
14
+ export const snippetRead: SnippetReadHook<PluginOptions> = async (
15
+ data,
16
+ { helpers },
17
+ ) => {
18
+ const { fieldPath } = data;
19
+
20
+ const label = "React";
21
+
22
+ switch (data.model.type) {
23
+ case prismicT.CustomTypeModelFieldType.Link: {
24
+ return {
25
+ label,
26
+ language: "tsx",
27
+ code: await helpers.format(
28
+ stripIndent`
29
+ <PrismicLink field={${dotPath(fieldPath)}}>Link</PrismicLink>
30
+ `,
31
+ undefined,
32
+ { prettier: prettierOptions },
33
+ ),
34
+ };
35
+ }
36
+
37
+ case prismicT.CustomTypeModelFieldType.Image: {
38
+ return [
39
+ {
40
+ label: `${label} (next/image)`,
41
+ language: "tsx",
42
+ code: await helpers.format(
43
+ stripIndent`
44
+ <PrismicNextImage field={${dotPath(fieldPath)}} />
45
+ `,
46
+ undefined,
47
+ { prettier: prettierOptions },
48
+ ),
49
+ },
50
+ {
51
+ label,
52
+ language: "tsx",
53
+ code: await helpers.format(
54
+ stripIndent`
55
+ <PrismicImage field={${dotPath(fieldPath)}} />
56
+ `,
57
+ undefined,
58
+ { prettier: prettierOptions },
59
+ ),
60
+ },
61
+ ];
62
+ }
63
+
64
+ case prismicT.CustomTypeModelFieldType.Group: {
65
+ const code = await helpers.format(
66
+ stripIndent`
67
+ <>{${dotPath(fieldPath)}.map(item => (
68
+ <>{/* Render content for item */}</>
69
+ ))}</>
70
+ `,
71
+ undefined,
72
+ { prettier: prettierOptions },
73
+ );
74
+
75
+ return {
76
+ label,
77
+ language: "tsx",
78
+ code,
79
+ };
80
+ }
81
+
82
+ case prismicT.CustomTypeModelFieldType.Slices: {
83
+ const code = await helpers.format(
84
+ stripIndent`
85
+ <SliceZone
86
+ slices={${dotPath(fieldPath)}}
87
+ components={components}
88
+ />
89
+ `,
90
+ undefined,
91
+ { prettier: prettierOptions },
92
+ );
93
+
94
+ return {
95
+ label,
96
+ language: "tsx",
97
+ code,
98
+ };
99
+ }
100
+
101
+ default: {
102
+ return {
103
+ label,
104
+ language: "tsx",
105
+ code: await helpers.format(
106
+ stripIndent`
107
+ <>{${dotPath(fieldPath)}}</>
108
+ `,
109
+ undefined,
110
+ { prettier: prettierOptions },
111
+ ),
112
+ };
113
+ }
114
+ }
115
+ };
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { plugin } from "./plugin";
2
+ export default plugin;
3
+
4
+ export type { PluginOptions } from "./types";
5
+
6
+ export { SliceSimulator } from "./SliceSimulator";
7
+ export type {
8
+ SliceSimulatorProps,
9
+ SliceSimulatorSliceZoneProps,
10
+ } from "./SliceSimulator";
@@ -0,0 +1,38 @@
1
+ import { SliceMachineContext } from "@slicemachine/plugin-kit";
2
+ import { stripIndent } from "common-tags";
3
+
4
+ import { PluginOptions } from "../types";
5
+
6
+ import { pascalCase } from "./pascalCase";
7
+
8
+ type BuildSliceLibraryIndexFileContentsArgs = {
9
+ libraryID: string;
10
+ } & SliceMachineContext<PluginOptions>;
11
+
12
+ export const buildSliceLibraryIndexFileContents = async (
13
+ args: BuildSliceLibraryIndexFileContentsArgs,
14
+ ): Promise<{ filePath: string; contents: string }> => {
15
+ const filePath = args.helpers.joinPathFromRoot(
16
+ args.libraryID,
17
+ args.options.typescript ? "index.ts" : "index.js",
18
+ );
19
+ const sliceLibrary = await args.actions.readSliceLibrary({
20
+ libraryID: args.libraryID,
21
+ });
22
+
23
+ let contents = stripIndent`
24
+ import dynamic from 'next/dynamic'
25
+
26
+ export const components = {
27
+ ${sliceLibrary.sliceIDs.map(
28
+ (id) => `${id}: dynamic(() => import('./${pascalCase(id)}')),`,
29
+ )}
30
+ }
31
+ `;
32
+
33
+ if (args.options.format) {
34
+ contents = await args.helpers.format(contents, filePath);
35
+ }
36
+
37
+ return { filePath, contents };
38
+ };
@@ -0,0 +1,13 @@
1
+ import type { PluginOptions } from "../types";
2
+
3
+ export const getJSOrTSXFileExtension = (
4
+ pluginOptions: PluginOptions,
5
+ ): string => {
6
+ if (pluginOptions.typescript) {
7
+ return "tsx";
8
+ } else if (pluginOptions.jsxExtension) {
9
+ return "jsx";
10
+ } else {
11
+ return "js";
12
+ }
13
+ };
@@ -0,0 +1,12 @@
1
+ import { pascalCase as basePascalCase } from "pascal-case";
2
+
3
+ /**
4
+ * Converts a string to a Pascal cased string.
5
+ *
6
+ * @param input - String to convert into a Pascal cased string.
7
+ *
8
+ * @returns Pascal cased string version of `input`.
9
+ */
10
+ export const pascalCase = (...input: (string | undefined)[]): string => {
11
+ return basePascalCase(input.filter(Boolean).join(" "));
12
+ };
@@ -0,0 +1,7 @@
1
+ import * as fs from "node:fs/promises";
2
+
3
+ export const readJSONFile = async <T = unknown>(path: string): Promise<T> => {
4
+ const contents = await fs.readFile(path, "utf8");
5
+
6
+ return JSON.parse(contents);
7
+ };
package/src/plugin.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { defineSliceMachinePlugin } from "@slicemachine/plugin-kit";
2
+
3
+ import { name as pkgName } from "../package.json";
4
+ import { PluginOptions } from "./types";
5
+
6
+ import { sliceCreate } from "./hooks/slice-create";
7
+ import { sliceUpdate } from "./hooks/slice-update";
8
+ import { sliceDelete } from "./hooks/slice-delete";
9
+ import { sliceRead } from "./hooks/slice-read";
10
+ import { sliceLibraryRead } from "./hooks/slice-library-read";
11
+ import { customTypeCreate } from "./hooks/customType-create";
12
+ import { customTypeUpdate } from "./hooks/customType-update";
13
+ import { customTypeDelete } from "./hooks/customType-delete";
14
+ import { customTypeRead } from "./hooks/customType-read";
15
+ import { customTypeLibraryRead } from "./hooks/customType-library-read";
16
+ import { snippetRead } from "./hooks/snippet-read";
17
+ import { sliceSimulatorSetupRead } from "./hooks/sliceSimulator-setup-read";
18
+
19
+ export const plugin = defineSliceMachinePlugin<PluginOptions>({
20
+ meta: {
21
+ name: pkgName,
22
+ },
23
+ defaultOptions: {
24
+ format: true,
25
+ },
26
+ setup({ hook }) {
27
+ hook("slice:create", sliceCreate);
28
+ hook("slice:update", sliceUpdate);
29
+ hook("slice:delete", sliceDelete);
30
+ hook("slice:read", sliceRead);
31
+ hook("slice-library:read", sliceLibraryRead);
32
+
33
+ hook("custom-type:create", customTypeCreate);
34
+ hook("custom-type:update", customTypeUpdate);
35
+ hook("custom-type:delete", customTypeDelete);
36
+ hook("custom-type:read", customTypeRead);
37
+ hook("custom-type-library:read", customTypeLibraryRead);
38
+
39
+ hook("snippet:read", snippetRead);
40
+
41
+ hook("slice-simulator:setup:read", sliceSimulatorSetupRead);
42
+ },
43
+ });
package/src/types.ts ADDED
@@ -0,0 +1,12 @@
1
+ export type PluginOptions = {
2
+ format?: boolean;
3
+ } & (
4
+ | {
5
+ typescript?: false;
6
+ jsxExtension?: boolean;
7
+ }
8
+ | {
9
+ typescript: true;
10
+ jsxExtension?: never;
11
+ }
12
+ );