storyloco 2.0.0 → 2.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyloco",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "A collection of slick Storyblok field plugins built with Svelte 5, TypeScript, and Tailwind CSS",
5
5
  "keywords": [
6
6
  "storyblok",
@@ -100,119 +100,170 @@ async function format_code(filename, content) {
100
100
  */
101
101
  export default function schema({ output_path, interval_ms, storyblok_personal_access_token, storyblok_space_id, filename, } = {}) {
102
102
  let interval = null;
103
+ let mode = "development";
104
+ let build_run = null;
105
+ // ensure output_path does not end with a slash
106
+ const out_dir = output_path?.replace(/\/?$/, "") ?? "src/lib";
107
+ const schema_ts_file = path.join(out_dir, filename ?? "components.schema.ts");
108
+ async function generate() {
109
+ const env = loadEnv(mode, process.cwd(), "");
110
+ const token = storyblok_personal_access_token ?? env.STORYBLOK_PERSONAL_ACCESS_TOKEN;
111
+ const space_id = storyblok_space_id ?? env.STORYBLOK_SPACE_ID;
112
+ // Skip rather than throw: plenty of builds (CI without secrets, contributors
113
+ // without Storyblok access) have no business failing over generated types.
114
+ if (!token || !space_id) {
115
+ logger.warn("STORYBLOK_PERSONAL_ACCESS_TOKEN and STORYBLOK_SPACE_ID are both required to generate component types — skipping");
116
+ return;
117
+ }
118
+ const x = await exec();
119
+ const components_file = path.join(".svelte-kit/storyblok/components", space_id, "components.json");
120
+ try {
121
+ // Check if components file exists
122
+ let existing_components = "";
123
+ try {
124
+ await access(components_file, constants.F_OK);
125
+ existing_components = await readFile(components_file, "utf-8");
126
+ }
127
+ catch {
128
+ // No existing file, that's fine
129
+ }
130
+ // Fetch the new components
131
+ const url = `https://mapi.storyblok.com/v1/spaces/${space_id}/components`;
132
+ const headers = new Headers({ Authorization: token });
133
+ const response = await fetch(url, { headers });
134
+ const { components: new_components } = await response.json();
135
+ // Make the file writable
136
+ await $ `chmod u+w ${components_file}`.quiet().nothrow();
137
+ // Nothing to do only if the schema is unchanged *and* the output is still there —
138
+ // checking the cache alone means a deleted output file never comes back.
139
+ const output_exists = await access(schema_ts_file, constants.F_OK).then(() => true, () => false);
140
+ if (output_exists &&
141
+ existing_components &&
142
+ JSON.stringify(JSON.parse(existing_components)) === JSON.stringify(new_components))
143
+ return;
144
+ // Create the components directory if it doesn't exist
145
+ await mkdir(path.dirname(components_file), { recursive: true });
146
+ // Write the data to the components file
147
+ await writeFile(components_file, JSON.stringify(new_components, null, 2));
148
+ const here = (str) => process.env.SCHEMA_DEV
149
+ ? path.join("src/lib/schema", str)
150
+ : path.join("node_modules/storyloco/packages/vite/dist/schema", str);
151
+ // Generate types
152
+ logger.start("Generating TypeScript definitions...");
153
+ await $ `chmod u+w ${schema_ts_file}`.quiet().nothrow();
154
+ await $ `${x} storyblok@4.6.6 ts generate -s ${space_id} -p .svelte-kit/storyblok --compiler-options ${here("compiler_options.js")} --custom-fields-parser ${here("custom_fields_parser.js")} --strict`.quiet();
155
+ logger.succeed("TypeScript definitions generated.");
156
+ // Get generated files
157
+ let component_types;
158
+ let general;
159
+ try {
160
+ component_types = await readFile(`.svelte-kit/storyblok/types/${space_id}/storyblok-components.d.ts`, "utf-8");
161
+ }
162
+ catch {
163
+ logger.warn("Component types file not found, skipping type generation");
164
+ return;
165
+ }
166
+ try {
167
+ general = await readFile(".svelte-kit/storyblok/types/storyblok.d.ts", "utf-8");
168
+ }
169
+ catch {
170
+ logger.warn("General types file not found, skipping type generation");
171
+ return;
172
+ }
173
+ const general_replaced = general.replace(`// This file was generated by the Storyblok CLI.
174
+ // DO NOT MODIFY THIS FILE BY HAND.
175
+ import type { ISbStoryData } from '@storyblok/js';`, "");
176
+ // determine how many folders are in output_path
177
+ const output_path_parts = out_dir.split("/");
178
+ const import_path = output_path_parts.map(() => `..`).join("/");
179
+ // Replicates the storyblok CLI's `getComponentType` (no prefix/suffix), so these
180
+ // entries reference the interface names it generates below.
181
+ const type_name = (component) => {
182
+ const sanitized = component
183
+ .replace(/[^a-z0-9]/gi, "_")
184
+ .replace(/_+/g, "_")
185
+ .replace(/^_+|_+$/g, "");
186
+ const camel = sanitized
187
+ .replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
188
+ .replace(/_/g, "");
189
+ const pascal = camel ? camel[0].toUpperCase() + camel.slice(1) : camel;
190
+ return /^\d/.test(pascal) ? `_${pascal}` : pascal;
191
+ };
192
+ const content_types = new_components
193
+ .filter((component) => component.is_root)
194
+ .map((component) => component.name)
195
+ .sort();
196
+ const header = `
197
+ /**
198
+ * AUTO-GENERATED FILE. DO NOT EDIT.
199
+ * Generated by ${name} plugin on ${new Date().toISOString()}.
200
+ * Any changes will be overwritten.
201
+ */
202
+
203
+ import type { StoryblokCustomPlugins } from "${import_path}/vite.config.js"
204
+
205
+ /**
206
+ * No \`SbBlokData\` intersection: its string index signature would reject the
207
+ * generated interfaces, which (deliberately) declare no index signature.
208
+ */
209
+ export type Blok<T> = T & { component?: string; _uid?: string; _editable?: string }
210
+
211
+ /**
212
+ * Content-type (root) components keyed by component name — the bloks a story
213
+ * can be. The CLI types every component identically, so this comes from the
214
+ * space's component list instead.
215
+ */
216
+ export interface ContentTypes {
217
+ ${content_types.map((name) => `"${name}": ${type_name(name)}`).join("\n")}
218
+ }
219
+
220
+ export type ContentType = ContentTypes[keyof ContentTypes]
221
+ `;
222
+ let content = component_types.replace(`// This file was generated by the storyblok CLI.
223
+ // DO NOT MODIFY THIS FILE BY HAND.`, header);
224
+ content = content.replace(/^.*\sfrom\s+'\.{2}\/storyblok\.d\.ts';\s*$/gm, "");
225
+ content = content + general_replaced;
226
+ content = content.replace(/\["StoryblokCustomPlugins/g, 'StoryblokCustomPlugins["');
227
+ // Try and use unknown instead of any
228
+ content = content.replace(/Record<string, any>/g, "Record<string, unknown>");
229
+ content = content.replace(/any\[]/g, "unknown[]");
230
+ // Make _uid and component optional (helps us reuse Storyblok types for non-storyblok components)
231
+ content = content.replace(/^([a-zA-Z0-9_]+):/gm, "$1?:");
232
+ const formatted = await format_code(path.basename(schema_ts_file), content);
233
+ // Write the final file
234
+ logger.start("Finalising schema file...");
235
+ await writeFile(schema_ts_file, formatted);
236
+ // Lock the file
237
+ await $ `chmod a-w ${schema_ts_file}`.quiet();
238
+ logger.succeed(`Storyblok component types generated successfully to ${Logger.color.magenta(schema_ts_file)}`);
239
+ }
240
+ catch (err) {
241
+ const message = err instanceof Error ? err.message : "An unknown error occurred.";
242
+ logger.fail(message);
243
+ }
244
+ }
103
245
  return {
104
246
  name,
247
+ configResolved(config) {
248
+ mode = config.mode;
249
+ },
250
+ /**
251
+ * Types are needed by anything that type-checks a build — CI especially, where
252
+ * the generated file is gitignored and no dev server ever runs. Vite builds each
253
+ * environment separately, so this hook fires more than once; generate once and
254
+ * let the later calls await the same run.
255
+ */
256
+ async buildStart() {
257
+ build_run ??= generate();
258
+ await build_run;
259
+ },
105
260
  async configureServer(server) {
106
- const env = loadEnv(server.config.mode, process.cwd(), "");
107
- const x = await exec();
261
+ mode = server.config.mode;
108
262
  interval_ms = interval_ms ?? 60000; // 1 minute
109
- // ensure output_path does not end with a slash
110
- output_path = output_path?.replace(/\/?$/, "") ?? "src/lib";
111
- storyblok_personal_access_token =
112
- storyblok_personal_access_token ?? env.STORYBLOK_PERSONAL_ACCESS_TOKEN;
113
- storyblok_space_id = storyblok_space_id ?? env.STORYBLOK_SPACE_ID;
114
- const components_file = path.join(".svelte-kit/storyblok/components", storyblok_space_id, "components.json");
115
- const schema_ts_file = path.join(output_path, filename ?? "components.schema.ts");
116
263
  const seconds = interval_ms / 1000;
117
264
  logger.info(`Regenerating Storyblok component types every ${seconds} ${seconds === 1 ? "second" : "seconds"}`);
118
- generate();
265
+ await generate();
119
266
  interval = setInterval(generate, interval_ms);
120
- async function generate() {
121
- if (!storyblok_personal_access_token)
122
- throw new Error("Storyblok personal access token is required");
123
- if (!storyblok_space_id)
124
- throw new Error("Storyblok space ID is required");
125
- try {
126
- // Check if components file exists
127
- let existing_components = "";
128
- try {
129
- await access(components_file, constants.F_OK);
130
- existing_components = await readFile(components_file, "utf-8");
131
- }
132
- catch {
133
- // No existing file, that's fine
134
- }
135
- // Fetch the new components
136
- const url = `https://mapi.storyblok.com/v1/spaces/${storyblok_space_id}/components`;
137
- const headers = new Headers({ Authorization: storyblok_personal_access_token });
138
- const response = await fetch(url, { headers });
139
- const { components: new_components } = await response.json();
140
- // Make the file writable
141
- await $ `chmod u+w ${components_file}`.quiet().nothrow();
142
- // Compare existing and new components
143
- if (existing_components &&
144
- JSON.stringify(JSON.parse(existing_components)) === JSON.stringify(new_components))
145
- return;
146
- // Create the components directory if it doesn't exist
147
- await mkdir(path.dirname(components_file), { recursive: true });
148
- // Write the data to the components file
149
- await writeFile(components_file, JSON.stringify(new_components, null, 2));
150
- const here = (str) => process.env.SCHEMA_DEV
151
- ? path.join("src/lib/schema", str)
152
- : path.join("node_modules/storyloco/packages/vite/dist/schema", str);
153
- // Generate types
154
- logger.start("Generating TypeScript definitions...");
155
- await $ `chmod u+w ${schema_ts_file}`.quiet().nothrow();
156
- await $ `${x} storyblok@4.6.6 ts generate -s ${storyblok_space_id} -p .svelte-kit/storyblok --compiler-options ${here("compiler_options.js")} --custom-fields-parser ${here("custom_fields_parser.js")} --strict`.quiet();
157
- logger.succeed("TypeScript definitions generated.");
158
- // Get generated files
159
- let component_types;
160
- let general;
161
- try {
162
- component_types = await readFile(`.svelte-kit/storyblok/types/${storyblok_space_id}/storyblok-components.d.ts`, "utf-8");
163
- }
164
- catch (error) {
165
- logger.warn("Component types file not found, skipping type generation");
166
- return;
167
- }
168
- try {
169
- general = await readFile(".svelte-kit/storyblok/types/storyblok.d.ts", "utf-8");
170
- }
171
- catch (error) {
172
- logger.warn("General types file not found, skipping type generation");
173
- return;
174
- }
175
- const general_replaced = general.replace(`// This file was generated by the Storyblok CLI.
176
- // DO NOT MODIFY THIS FILE BY HAND.
177
- import type { ISbStoryData } from '@storyblok/js';`, "");
178
- // determine how many folders are in output_path
179
- const output_path_parts = output_path?.split("/") ?? [];
180
- const import_path = output_path_parts.map(() => `..`).join("/");
181
- const header = `
182
- /**
183
- * AUTO-GENERATED FILE. DO NOT EDIT.
184
- * Generated by ${name} plugin on ${new Date().toISOString()}.
185
- * Any changes will be overwritten.
186
- */
187
-
188
- import type { StoryblokCustomPlugins } from "${import_path}/vite.config.js"
189
- import type { SbBlokData } from "@storyblok/svelte"
190
-
191
- export type Blok<T> = SbBlokData & T
192
- `;
193
- let content = component_types.replace(`// This file was generated by the storyblok CLI.
194
- // DO NOT MODIFY THIS FILE BY HAND.`, header);
195
- content = content.replace(/^.*\sfrom\s+'\.{2}\/storyblok\.d\.ts';\s*$/gm, "");
196
- content = content + general_replaced;
197
- content = content.replace(/\["StoryblokCustomPlugins/g, 'StoryblokCustomPlugins["');
198
- // Try and use unknown instead of any
199
- content = content.replace(/Record<string, any>/g, "Record<string, unknown>");
200
- content = content.replace(/any\[]/g, "unknown[]");
201
- // Make _uid and component optional (helps us reuse Storyblok types for non-storyblok components)
202
- content = content.replace(/^([a-zA-Z0-9_]+):/gm, "$1?:");
203
- const formatted = await format_code(path.basename(schema_ts_file), content);
204
- // Write the final file
205
- logger.start("Finalising schema file...");
206
- await writeFile(schema_ts_file, formatted);
207
- // Lock the file
208
- await $ `chmod a-w ${schema_ts_file}`.quiet();
209
- logger.succeed(`Storyblok component types generated successfully to ${Logger.color.magenta(schema_ts_file)}`);
210
- }
211
- catch (err) {
212
- const message = err instanceof Error ? err.message : "An unknown error occurred.";
213
- logger.fail(message);
214
- }
215
- }
216
267
  },
217
268
  closeBundle() {
218
269
  if (interval)
@@ -196,7 +196,10 @@ interface BlokFields {
196
196
  * </section>
197
197
  * ```
198
198
  */
199
- export declare function attrs<T extends BlokFields>(blok: SbBlokData & T): {
199
+ export declare function attrs<T extends BlokFields>(blok: T & {
200
+ _uid?: string;
201
+ _editable?: string;
202
+ }): {
200
203
  [x: symbol]: (node: HTMLElement) => void;
201
204
  id: string | undefined;
202
205
  };
@@ -186,6 +186,7 @@ export function handle_error(err) {
186
186
  export function attrs(blok) {
187
187
  return {
188
188
  [attach()](node) {
189
+ // SbBlokData is nominal ceremony here — editable only reads `_editable`.
189
190
  editable(node, blok);
190
191
  },
191
192
  id: blok.anchor || blok._uid,