@withstudiocms/internal_helpers 0.1.0-beta.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present StudioCMS - withstudiocms (https://github.com/withstudiocms)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @withstudiocms/internal_helpers
2
+
3
+ Internal helpers and utilities for the @withstudiocms eco-system.
4
+
5
+ ## License
6
+
7
+ [MIT Licensed](./LICENSE)
@@ -0,0 +1,24 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ /**
3
+ * Easily add a list of integrations from within an integration.
4
+ *
5
+ * @param {import("astro").HookParameters<"astro:config:setup">} params
6
+ * @param {array} integrations
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import Vue from "@astrojs/vue";
11
+ * import tailwindcss from "@astrojs/tailwind";
12
+ *
13
+ * addIntegrationArray(params, [
14
+ * { integration: Vue(), ensureUnique: true }
15
+ * { integration: tailwindcss() }
16
+ * ])
17
+ * ```
18
+ *
19
+ * @see https://astro-integration-kit.netlify.app/utilities/add-integration/
20
+ */
21
+ export declare const addIntegrationArray: import("astro-integration-kit").HookUtility<"astro:config:setup", [integrations: {
22
+ integration: AstroIntegration;
23
+ ensureUnique?: boolean | undefined;
24
+ }[]], void>;
@@ -0,0 +1,11 @@
1
+ import { addIntegration, defineUtility } from "astro-integration-kit";
2
+ const addIntegrationArray = defineUtility("astro:config:setup")(
3
+ (params, integrations) => {
4
+ for (const { integration, ensureUnique } of integrations) {
5
+ addIntegration(params, { integration, ensureUnique });
6
+ }
7
+ }
8
+ );
9
+ export {
10
+ addIntegrationArray
11
+ };
@@ -0,0 +1,12 @@
1
+ import type { AstroIntegrationLogger } from 'astro';
2
+ /**
3
+ * Fetches the latest version of a given npm package from the npm registry.
4
+ *
5
+ * @param packageName - The name of the npm package to fetch the latest version for.
6
+ * @param logger - An instance of `AstroIntegrationLogger` used to log errors if the fetch fails.
7
+ * @returns A promise that resolves to the latest version of the package as a string,
8
+ * or `null` if an error occurs during the fetch process.
9
+ *
10
+ * @throws Will throw an error if the HTTP response from the npm registry is not successful.
11
+ */
12
+ export declare function getLatestVersion(packageName: string, logger: AstroIntegrationLogger, cacheJsonFile: URL | undefined, isDevMode: boolean): Promise<string | null>;
@@ -0,0 +1,36 @@
1
+ import fs from "node:fs";
2
+ import { jsonParse } from "../utils/jsonUtils.js";
3
+ async function getLatestVersion(packageName, logger, cacheJsonFile, isDevMode) {
4
+ let cacheData = {};
5
+ if (isDevMode && cacheJsonFile) {
6
+ const file = fs.readFileSync(cacheJsonFile, { encoding: "utf-8" });
7
+ cacheData = jsonParse(file);
8
+ if (cacheData.latestVersionCheck?.lastChecked && new Date(cacheData.latestVersionCheck.lastChecked).getTime() > new Date(Date.now() - 60 * 60 * 1e3).getTime()) {
9
+ return cacheData.latestVersionCheck.version;
10
+ }
11
+ }
12
+ try {
13
+ const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
14
+ if (!response.ok) {
15
+ throw new Error(`Failed to fetch package info: ${response.statusText}`);
16
+ }
17
+ const data = await response.json();
18
+ if (isDevMode && cacheJsonFile) {
19
+ const updatedCacheData = {
20
+ ...cacheData,
21
+ latestVersionCheck: {
22
+ lastChecked: /* @__PURE__ */ new Date(),
23
+ version: data.version
24
+ }
25
+ };
26
+ fs.writeFileSync(cacheJsonFile, JSON.stringify(updatedCacheData, null, 2), "utf-8");
27
+ }
28
+ return data.version;
29
+ } catch (error) {
30
+ logger.error(`Error fetching latest version of ${packageName}: ${error}`);
31
+ return null;
32
+ }
33
+ }
34
+ export {
35
+ getLatestVersion
36
+ };
@@ -0,0 +1,4 @@
1
+ export * from './addIntegrationArray.js';
2
+ export * from './getLatestVersion.js';
3
+ export * from './injectScripts.js';
4
+ export * from './integrationLogger.js';
@@ -0,0 +1,4 @@
1
+ export * from "./addIntegrationArray.js";
2
+ export * from "./getLatestVersion.js";
3
+ export * from "./injectScripts.js";
4
+ export * from "./integrationLogger.js";
@@ -0,0 +1,26 @@
1
+ import type { InjectedScriptStage } from 'astro';
2
+ /**
3
+ * Represents a script to be injected at a specific stage of the integration process.
4
+ *
5
+ * @property stage - The stage at which the script should be injected.
6
+ * @property content - The actual script content as a string.
7
+ * @property enabled - Indicates whether the script is enabled for injection.
8
+ */
9
+ export interface ScriptEntry {
10
+ stage: InjectedScriptStage;
11
+ content: string;
12
+ enabled: boolean;
13
+ }
14
+ /**
15
+ * Injects scripts into the Astro configuration setup stage.
16
+ *
17
+ * This utility iterates over an array of script entries and injects each enabled script
18
+ * at the specified stage using the provided `params.injectScript` method.
19
+ *
20
+ * @param params - The parameters provided by the Astro integration context, including the `injectScript` function.
21
+ * @param entries - An array of script entries to be injected. Each entry contains:
22
+ * - `stage`: The stage at which the script should be injected.
23
+ * - `content`: The script content to inject.
24
+ * - `enabled`: A boolean indicating whether the script should be injected.
25
+ */
26
+ export declare const injectScripts: import("astro-integration-kit").HookUtility<"astro:config:setup", [entries: ScriptEntry[]], void>;
@@ -0,0 +1,12 @@
1
+ import { defineUtility } from "astro-integration-kit";
2
+ const injectScripts = defineUtility("astro:config:setup")(
3
+ ({ injectScript }, entries) => {
4
+ for (const { enabled, stage, content } of entries) {
5
+ if (!enabled) continue;
6
+ injectScript(stage, content);
7
+ }
8
+ }
9
+ );
10
+ export {
11
+ injectScripts
12
+ };
@@ -0,0 +1,59 @@
1
+ import type { AstroIntegrationLogger } from 'astro';
2
+ /**
3
+ * Represents an array of message objects.
4
+ * Each message object contains information about a log message.
5
+ *
6
+ * @property {string} label - The label associated with the message.
7
+ * @property {'info' | 'warn' | 'error' | 'debug'} logLevel - The level of the log message.
8
+ * @property {string} message - The content of the log message.
9
+ */
10
+ export type Messages = {
11
+ label: string;
12
+ logLevel: 'info' | 'warn' | 'error' | 'debug';
13
+ message: string;
14
+ }[];
15
+ /**
16
+ * Options for configuring the integration logger.
17
+ *
18
+ * @property logLevel - The minimum level of messages to log. Can be 'info', 'warn', 'error', or 'debug'.
19
+ * @property logger - The logger instance to use for logging messages.
20
+ * @property verbose - Optional flag to enable verbose logging output.
21
+ */
22
+ export type LoggerOpts = {
23
+ logLevel: 'info' | 'warn' | 'error' | 'debug';
24
+ logger: AstroIntegrationLogger;
25
+ verbose?: boolean;
26
+ };
27
+ /**
28
+ * Logs a message using the provided logger and log level, with optional verbosity control.
29
+ *
30
+ * @param opts - An object containing logger options:
31
+ * - `logLevel`: The level at which to log the message (e.g., 'debug', 'info', 'warn', 'error').
32
+ * - `logger`: The logger instance with methods corresponding to log levels.
33
+ * - `verbose`: If true, always logs the message; if false, only logs for levels other than 'debug' and 'info'.
34
+ * @param message - The message to be logged.
35
+ * @returns A promise that resolves when logging is complete.
36
+ */
37
+ export declare const integrationLogger: (opts: LoggerOpts, message: string) => Promise<void>;
38
+ /**
39
+ * Creates a new `AstroIntegrationLogger` instance scoped to a specific plugin.
40
+ *
41
+ * @param id - The unique identifier for the plugin.
42
+ * @param logger - The base `AstroIntegrationLogger` to fork from.
43
+ * @returns A new `AstroIntegrationLogger` instance with a namespace prefixed by `plugin:{id}`.
44
+ */
45
+ export declare function pluginLogger(id: string, logger: AstroIntegrationLogger): AstroIntegrationLogger;
46
+ /**
47
+ * Logs a series of messages using the provided Astro integration logger.
48
+ *
49
+ * @param messages - An array of message objects, each containing a label, message, and log level.
50
+ * @param options - Options for logging, including a `verbose` flag to control verbosity.
51
+ * @param logger - The Astro integration logger instance used for logging messages.
52
+ *
53
+ * @remarks
54
+ * Each message is logged with a forked logger using its label. The verbosity of 'info' level logs
55
+ * is controlled by the `options.verbose` flag, while other log levels are always logged.
56
+ */
57
+ export declare function logMessages(messages: Messages, options: {
58
+ verbose: boolean;
59
+ }, logger: AstroIntegrationLogger): Promise<void>;
@@ -0,0 +1,36 @@
1
+ const integrationLogger = async (opts, message) => {
2
+ const { logLevel, logger, verbose } = opts;
3
+ switch (verbose) {
4
+ case true:
5
+ logger[logLevel](message);
6
+ break;
7
+ case false:
8
+ if (logLevel !== "debug" && logLevel !== "info") {
9
+ logger[logLevel](message);
10
+ }
11
+ break;
12
+ default:
13
+ logger[logLevel](message);
14
+ }
15
+ };
16
+ function pluginLogger(id, logger) {
17
+ const newLogger = logger.fork(`plugin:${id}`);
18
+ return newLogger;
19
+ }
20
+ async function logMessages(messages, options, logger) {
21
+ for (const { label, message, logLevel } of messages) {
22
+ await integrationLogger(
23
+ {
24
+ logger: logger.fork(label),
25
+ logLevel,
26
+ verbose: logLevel === "info" ? options.verbose : true
27
+ },
28
+ message
29
+ );
30
+ }
31
+ }
32
+ export {
33
+ integrationLogger,
34
+ logMessages,
35
+ pluginLogger
36
+ };
@@ -0,0 +1,71 @@
1
+ import { z } from 'astro/zod';
2
+ export declare const HeadConfigSchema: () => z.ZodDefault<z.ZodArray<z.ZodObject<{
3
+ /** Name of the HTML tag to add to `<head>`, e.g. `'meta'`, `'link'`, or `'script'`. */
4
+ tag: z.ZodEnum<["title", "base", "link", "style", "meta", "script", "noscript", "template"]>;
5
+ /** Attributes to set on the tag, e.g. `{ rel: 'stylesheet', href: '/custom.css' }`. */
6
+ attrs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodBoolean, z.ZodUndefined]>>>;
7
+ /** Content to place inside the tag (optional). */
8
+ content: z.ZodDefault<z.ZodString>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ tag: "title" | "base" | "link" | "style" | "meta" | "script" | "noscript" | "template";
11
+ attrs: Record<string, string | boolean | undefined>;
12
+ content: string;
13
+ }, {
14
+ tag: "title" | "base" | "link" | "style" | "meta" | "script" | "noscript" | "template";
15
+ attrs?: Record<string, string | boolean | undefined> | undefined;
16
+ content?: string | undefined;
17
+ }>, "many">>;
18
+ export type HeadUserConfig = z.input<ReturnType<typeof HeadConfigSchema>>;
19
+ export type HeadConfig = z.output<ReturnType<typeof HeadConfigSchema>>;
20
+ export declare const HeadSchema: z.ZodDefault<z.ZodArray<z.ZodObject<{
21
+ /** Name of the HTML tag to add to `<head>`, e.g. `'meta'`, `'link'`, or `'script'`. */
22
+ tag: z.ZodEnum<["title", "base", "link", "style", "meta", "script", "noscript", "template"]>;
23
+ /** Attributes to set on the tag, e.g. `{ rel: 'stylesheet', href: '/custom.css' }`. */
24
+ attrs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodBoolean, z.ZodUndefined]>>>;
25
+ /** Content to place inside the tag (optional). */
26
+ content: z.ZodDefault<z.ZodString>;
27
+ }, "strip", z.ZodTypeAny, {
28
+ tag: "title" | "base" | "link" | "style" | "meta" | "script" | "noscript" | "template";
29
+ attrs: Record<string, string | boolean | undefined>;
30
+ content: string;
31
+ }, {
32
+ tag: "title" | "base" | "link" | "style" | "meta" | "script" | "noscript" | "template";
33
+ attrs?: Record<string, string | boolean | undefined> | undefined;
34
+ content?: string | undefined;
35
+ }>, "many">>;
36
+ /** Create a fully parsed, merged, and sorted head entry array from multiple sources. */
37
+ export declare function createHead(defaults: HeadUserConfig, ...heads: HeadConfig[]): {
38
+ tag: "title" | "base" | "link" | "style" | "meta" | "script" | "noscript" | "template";
39
+ attrs: Record<string, string | boolean | undefined>;
40
+ content: string;
41
+ }[];
42
+ /**
43
+ * Test if a head config object contains a matching `<title>` or `<meta>` tag.
44
+ *
45
+ * For example, will return true if `head` already contains
46
+ * `<meta name="description" content="A">` and the passed `tag`
47
+ * is `<meta name="description" content="B">`. Tests against `name`,
48
+ * `property`, and `http-equiv` attributes for `<meta>` tags.
49
+ */
50
+ export declare function hasTag(head: HeadConfig, entry: HeadConfig[number]): boolean;
51
+ /**
52
+ * Test if a head config object contains a tag of the same type
53
+ * as `entry` and a matching attribute for one of the passed `keys`.
54
+ */
55
+ export declare function hasOneOf(head: HeadConfig, entry: HeadConfig[number], keys: string[]): boolean;
56
+ /** Find the first matching key–value pair in a head entry’s attributes. */
57
+ export declare function getAttr(keys: string[], entry: HeadConfig[number]): [key: string, value: string | boolean] | undefined;
58
+ /** Merge two heads, overwriting entries in the first head that exist in the second. */
59
+ export declare function mergeHead(oldHead: HeadConfig, newHead: HeadConfig): {
60
+ tag: "title" | "base" | "link" | "style" | "meta" | "script" | "noscript" | "template";
61
+ attrs: Record<string, string | boolean | undefined>;
62
+ content: string;
63
+ }[];
64
+ /** Sort head tags to place important tags first and relegate “SEO” meta tags. */
65
+ export declare function sortHead(head: HeadConfig): {
66
+ tag: "title" | "base" | "link" | "style" | "meta" | "script" | "noscript" | "template";
67
+ attrs: Record<string, string | boolean | undefined>;
68
+ content: string;
69
+ }[];
70
+ /** Get the relative importance of a specific head tag. */
71
+ export declare function getImportance(entry: HeadConfig[number]): 100 | 90 | 70 | 80 | 0;
@@ -0,0 +1,80 @@
1
+ import { z } from "astro/zod";
2
+ const HeadConfigSchema = () => z.array(
3
+ z.object({
4
+ /** Name of the HTML tag to add to `<head>`, e.g. `'meta'`, `'link'`, or `'script'`. */
5
+ tag: z.enum(["title", "base", "link", "style", "meta", "script", "noscript", "template"]),
6
+ /** Attributes to set on the tag, e.g. `{ rel: 'stylesheet', href: '/custom.css' }`. */
7
+ attrs: z.record(z.union([z.string(), z.boolean(), z.undefined()])).default({}),
8
+ /** Content to place inside the tag (optional). */
9
+ content: z.string().default("")
10
+ })
11
+ ).default([]);
12
+ const HeadSchema = HeadConfigSchema();
13
+ function createHead(defaults, ...heads) {
14
+ let head = HeadSchema.parse(defaults);
15
+ for (const next of heads) {
16
+ head = mergeHead(head, next);
17
+ }
18
+ return sortHead(head);
19
+ }
20
+ function hasTag(head, entry) {
21
+ switch (entry.tag) {
22
+ case "title":
23
+ return head.some(({ tag }) => tag === "title");
24
+ case "meta":
25
+ return hasOneOf(head, entry, ["name", "property", "http-equiv"]);
26
+ default:
27
+ return false;
28
+ }
29
+ }
30
+ function hasOneOf(head, entry, keys) {
31
+ const attr = getAttr(keys, entry);
32
+ if (!attr) return false;
33
+ const [key, val] = attr;
34
+ return head.some(({ tag, attrs }) => tag === entry.tag && attrs[key] === val);
35
+ }
36
+ function getAttr(keys, entry) {
37
+ let attr;
38
+ for (const key of keys) {
39
+ const val = entry.attrs[key];
40
+ if (val) {
41
+ attr = [key, val];
42
+ break;
43
+ }
44
+ }
45
+ return attr;
46
+ }
47
+ function mergeHead(oldHead, newHead) {
48
+ return [...oldHead.filter((tag) => !hasTag(newHead, tag)), ...newHead];
49
+ }
50
+ function sortHead(head) {
51
+ return head.sort((a, b) => {
52
+ const aImportance = getImportance(a);
53
+ const bImportance = getImportance(b);
54
+ return aImportance > bImportance ? -1 : bImportance > aImportance ? 1 : 0;
55
+ });
56
+ }
57
+ function getImportance(entry) {
58
+ if (entry.tag === "meta" && ("charset" in entry.attrs || "http-equiv" in entry.attrs || entry.attrs.name === "viewport")) {
59
+ return 100;
60
+ }
61
+ if (entry.tag === "title") return 90;
62
+ if (entry.tag !== "meta") {
63
+ if (entry.tag === "link" && "rel" in entry.attrs && entry.attrs.rel === "shortcut icon") {
64
+ return 70;
65
+ }
66
+ return 80;
67
+ }
68
+ return 0;
69
+ }
70
+ export {
71
+ HeadConfigSchema,
72
+ HeadSchema,
73
+ createHead,
74
+ getAttr,
75
+ getImportance,
76
+ hasOneOf,
77
+ hasTag,
78
+ mergeHead,
79
+ sortHead
80
+ };
@@ -0,0 +1 @@
1
+ export * from './utils/index.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from "./utils/index.js";
@@ -0,0 +1,20 @@
1
+ /** Get the a root-relative URL path with the site’s `base` prefixed. */
2
+ export declare function pathWithBase(path: string): string;
3
+ /** Get the a root-relative file URL path with the site’s `base` prefixed. */
4
+ export declare function fileWithBase(path: string): string;
5
+ /** Ensure the passed path starts with a leading slash. */
6
+ export declare function ensureLeadingSlash(href: string): string;
7
+ /** Ensure the passed path ends with a trailing slash. */
8
+ export declare function ensureTrailingSlash(href: string): string;
9
+ /** Ensure the passed path starts and ends with slashes. */
10
+ export declare function ensureLeadingAndTrailingSlashes(href: string): string;
11
+ /** Ensure the passed path does not start with a leading slash. */
12
+ export declare function stripLeadingSlash(href: string): string;
13
+ /** Ensure the passed path does not end with a trailing slash. */
14
+ export declare function stripTrailingSlash(href: string): string;
15
+ /** Ensure the passed path does not start and end with slashes. */
16
+ export declare function stripLeadingAndTrailingSlashes(href: string): string;
17
+ /** Remove the extension from a path. */
18
+ export declare function stripHtmlExtension(path: string): string;
19
+ /** Add '.html' extension to a path. */
20
+ export declare function ensureHtmlExtension(path: string): string;
@@ -0,0 +1,66 @@
1
+ function pathWithBase(path) {
2
+ let newPath = path;
3
+ newPath = stripLeadingSlash(newPath);
4
+ return newPath ? `/${newPath}` : "/";
5
+ }
6
+ function fileWithBase(path) {
7
+ let newPath = path;
8
+ newPath = stripLeadingSlash(newPath);
9
+ return newPath ? `/${newPath}` : "/";
10
+ }
11
+ function ensureLeadingSlash(href) {
12
+ let newHref = href;
13
+ if (newHref[0] !== "/") newHref = `/${newHref}`;
14
+ return newHref;
15
+ }
16
+ function ensureTrailingSlash(href) {
17
+ let newHref = href;
18
+ if (newHref[newHref.length - 1] !== "/") newHref += "/";
19
+ return newHref;
20
+ }
21
+ function ensureLeadingAndTrailingSlashes(href) {
22
+ let newHref = href;
23
+ newHref = ensureLeadingSlash(newHref);
24
+ newHref = ensureTrailingSlash(newHref);
25
+ return newHref;
26
+ }
27
+ function stripLeadingSlash(href) {
28
+ let newHref = href;
29
+ if (newHref[0] === "/") newHref = newHref.slice(1);
30
+ return newHref;
31
+ }
32
+ function stripTrailingSlash(href) {
33
+ let newHref = href;
34
+ if (newHref[newHref.length - 1] === "/") newHref = newHref.slice(0, -1);
35
+ return newHref;
36
+ }
37
+ function stripLeadingAndTrailingSlashes(href) {
38
+ let newHref = href;
39
+ newHref = stripLeadingSlash(newHref);
40
+ newHref = stripTrailingSlash(newHref);
41
+ return newHref;
42
+ }
43
+ function stripHtmlExtension(path) {
44
+ const pathWithoutTrailingSlash = stripTrailingSlash(path);
45
+ return pathWithoutTrailingSlash.endsWith(".html") ? pathWithoutTrailingSlash.slice(0, -5) : pathWithoutTrailingSlash;
46
+ }
47
+ function ensureHtmlExtension(path) {
48
+ let newPath = path;
49
+ newPath = stripLeadingAndTrailingSlashes(newPath);
50
+ if (!newPath.endsWith(".html")) {
51
+ newPath = newPath ? `${newPath}.html` : "/index.html";
52
+ }
53
+ return ensureLeadingSlash(newPath);
54
+ }
55
+ export {
56
+ ensureHtmlExtension,
57
+ ensureLeadingAndTrailingSlashes,
58
+ ensureLeadingSlash,
59
+ ensureTrailingSlash,
60
+ fileWithBase,
61
+ pathWithBase,
62
+ stripHtmlExtension,
63
+ stripLeadingAndTrailingSlashes,
64
+ stripLeadingSlash,
65
+ stripTrailingSlash
66
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Creates a URL generator factory function with a default dashboard route.
3
+ *
4
+ * The returned `urlGenFactory` function generates URLs based on whether the route is a dashboard route,
5
+ * an optional path, and an optional dashboard route override.
6
+ *
7
+ * @param defaultDashboardRoute - The default base route for dashboard URLs.
8
+ * @returns A factory function that generates URLs based on the provided parameters.
9
+ *
10
+ * @example
11
+ * const urlGen = createURLGenFactory('dashboard');
12
+ * urlGen(true, 'settings'); // Returns '/dashboard/settings'
13
+ * urlGen(false, 'about'); // Returns '/about'
14
+ */
15
+ export declare function createURLGenFactory(defaultDashboardRoute: string): (isDashboardRoute: boolean, path: string | undefined, DashboardRouteOverride?: string) => string;
@@ -0,0 +1,28 @@
1
+ import { pathWithBase, stripLeadingAndTrailingSlashes } from "./pathGenerators.js";
2
+ function createURLGenFactory(defaultDashboardRoute) {
3
+ return function urlGenFactory(isDashboardRoute, path, DashboardRouteOverride) {
4
+ let url;
5
+ let dashboardRoute = stripLeadingAndTrailingSlashes(defaultDashboardRoute);
6
+ if (DashboardRouteOverride) {
7
+ dashboardRoute = stripLeadingAndTrailingSlashes(DashboardRouteOverride);
8
+ }
9
+ if (path) {
10
+ const cleanPath = stripLeadingAndTrailingSlashes(path);
11
+ if (isDashboardRoute) {
12
+ url = pathWithBase(`${dashboardRoute}/${cleanPath}`);
13
+ } else {
14
+ url = pathWithBase(cleanPath);
15
+ }
16
+ } else {
17
+ if (isDashboardRoute) {
18
+ url = pathWithBase(dashboardRoute);
19
+ } else {
20
+ url = pathWithBase("");
21
+ }
22
+ }
23
+ return url;
24
+ };
25
+ }
26
+ export {
27
+ createURLGenFactory
28
+ };
@@ -0,0 +1,4 @@
1
+ export * from './jsonUtils.js';
2
+ export * from './loadChangelog.js';
3
+ export * from './pageTypeFilter.js';
4
+ export * from './safeString.js';
@@ -0,0 +1,4 @@
1
+ export * from "./jsonUtils.js";
2
+ export * from "./loadChangelog.js";
3
+ export * from "./pageTypeFilter.js";
4
+ export * from "./safeString.js";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Parses a JSON string and returns the resulting object.
3
+ *
4
+ * @template T - The expected type of the parsed object.
5
+ * @param text - The JSON string to parse.
6
+ * @returns The parsed object of type `T`.
7
+ * @throws {SyntaxError} If the input string is not valid JSON.
8
+ */
9
+ export declare function jsonParse<T extends object>(text: string): T;
10
+ /**
11
+ * Reads a JSON file and parses it into an object of type T.
12
+ */
13
+ export declare function readJson<T extends object>(path: string | URL): T;
@@ -0,0 +1,11 @@
1
+ import fs from "node:fs";
2
+ function jsonParse(text) {
3
+ return JSON.parse(text);
4
+ }
5
+ function readJson(path) {
6
+ return jsonParse(fs.readFileSync(path, "utf-8"));
7
+ }
8
+ export {
9
+ jsonParse,
10
+ readJson
11
+ };
@@ -0,0 +1,82 @@
1
+ import type { List } from 'mdast';
2
+ /**
3
+ * Represents a changelog entry for a specific version.
4
+ *
5
+ * @property version - The semantic version string (e.g., "1.2.3").
6
+ * @property changes - An object mapping each semantic version category to a list of changes.
7
+ * @property includes - A set of strings indicating included features or modules for this version.
8
+ */
9
+ export type Version = {
10
+ version: string;
11
+ changes: {
12
+ [key in SemverCategory]: List;
13
+ };
14
+ includes: Set<string>;
15
+ };
16
+ /**
17
+ * Represents the changelog for a package, including its name and a list of versions.
18
+ *
19
+ * @property packageName - The name of the package.
20
+ * @property versions - An array of version objects associated with the package.
21
+ */
22
+ export type Changelog = {
23
+ packageName: string;
24
+ versions: Version[];
25
+ };
26
+ /**
27
+ * Defines the semantic versioning categories used to classify changes.
28
+ * - `major`: Indicates breaking changes.
29
+ * - `minor`: Indicates backward-compatible feature additions.
30
+ * - `patch`: Indicates backward-compatible bug fixes.
31
+ */
32
+ export declare const semverCategories: readonly ["major", "minor", "patch"];
33
+ /**
34
+ * Represents a semantic versioning category, derived from the `semverCategories` array.
35
+ *
36
+ * This type is useful for constraining values to valid semantic version categories.
37
+ */
38
+ export type SemverCategory = (typeof semverCategories)[number];
39
+ /**
40
+ * Represents the raw source of a changelog.
41
+ *
42
+ * @property raw - The raw changelog content as a string.
43
+ */
44
+ type RawChangelogSrc = {
45
+ raw: string;
46
+ };
47
+ /**
48
+ * Represents a changelog to be loaded from a file.
49
+ *
50
+ * @property path - The file system path to the changelog file.
51
+ */
52
+ type FromFileChangelog = {
53
+ path: string;
54
+ };
55
+ /**
56
+ * Represents the source of a changelog, which can either be a raw changelog source
57
+ * or a changelog loaded from a file.
58
+ *
59
+ * @see RawChangelogSrc
60
+ * @see FromFileChangelog
61
+ */
62
+ export type ChangeLogSrc = RawChangelogSrc | FromFileChangelog;
63
+ /**
64
+ * Loads and parses a changelog from either a file path or a raw markdown string.
65
+ *
66
+ * - Reads the changelog markdown content from the provided source.
67
+ * - Converts GitHub usernames in "Thanks ..." sentences to markdown links.
68
+ * - Parses the markdown into an AST and extracts structured changelog information:
69
+ * - The package name (from the first-level heading).
70
+ * - Versions (from second-level headings).
71
+ * - Semantic version categories (from third-level headings).
72
+ * - Changes for each category, filtering out package references and dependency updates.
73
+ * - Tracks included package references for each version.
74
+ *
75
+ * Throws errors if the markdown structure does not match the expected format.
76
+ *
77
+ * @param src - The source of the changelog, either a file path or raw markdown.
78
+ * @returns The parsed changelog object containing package name, versions, and categorized changes.
79
+ * @throws {Error} If the markdown structure is unexpected or invalid.
80
+ */
81
+ export declare function loadChangelog(src: ChangeLogSrc): Changelog;
82
+ export {};
@@ -0,0 +1,109 @@
1
+ import fs from "node:fs";
2
+ import { fromMarkdown } from "mdast-util-from-markdown";
3
+ import { toString as ToString } from "mdast-util-to-string";
4
+ import { visit } from "unist-util-visit";
5
+ const semverCategories = ["major", "minor", "patch"];
6
+ function loadChangelog(src) {
7
+ let markdown;
8
+ if ("path" in src) {
9
+ markdown = fs.readFileSync(src.path, "utf8");
10
+ } else {
11
+ markdown = src.raw;
12
+ }
13
+ markdown = markdown.replace(
14
+ /(?<=Thank[^.!]*? )@([a-z0-9-]+)(?=[\s,.!])/gi,
15
+ "[@$1](https://github.com/$1)"
16
+ );
17
+ const ast = fromMarkdown(markdown);
18
+ const changelog = {
19
+ packageName: "",
20
+ versions: []
21
+ };
22
+ let state = "packageName";
23
+ let version;
24
+ let semverCategory;
25
+ function handleNode(node) {
26
+ if (node.type === "heading") {
27
+ if (node.depth === 1) {
28
+ if (state !== "packageName") throw new Error("Unexpected h1");
29
+ changelog.packageName = ToString(node);
30
+ state = "version";
31
+ return;
32
+ }
33
+ if (node.depth === 2) {
34
+ if (state === "packageName") throw new Error("Unexpected h2");
35
+ version = {
36
+ version: ToString(node),
37
+ changes: {
38
+ major: { type: "list", children: [] },
39
+ minor: { type: "list", children: [] },
40
+ patch: { type: "list", children: [] }
41
+ },
42
+ includes: /* @__PURE__ */ new Set()
43
+ };
44
+ changelog.versions.push(version);
45
+ state = "semverCategory";
46
+ return;
47
+ }
48
+ if (node.depth === 3) {
49
+ if (state === "packageName" || state === "version") throw new Error("Unexpected h3");
50
+ semverCategory = (ToString(node).split(" ")[0] || "").toLowerCase();
51
+ if (!semverCategories.includes(semverCategory))
52
+ throw new Error(`Unexpected semver category: ${semverCategory}`);
53
+ state = "changes";
54
+ return;
55
+ }
56
+ }
57
+ if (node.type === "list") {
58
+ if (state !== "changes" || !version || !semverCategory) throw new Error("Unexpected list");
59
+ for (let listItemIdx = 0; listItemIdx < node.children.length; listItemIdx++) {
60
+ const listItem = node.children[listItemIdx];
61
+ if (!listItem) continue;
62
+ const lastChild = listItem.children[listItem.children.length - 1];
63
+ if (lastChild?.type === "list") {
64
+ const packageRefs = [];
65
+ lastChild.children.forEach((subListItem) => {
66
+ const text = ToString(subListItem);
67
+ if (parsePackageReference(text)) packageRefs.push(text);
68
+ });
69
+ if (packageRefs.length === lastChild.children.length) {
70
+ for (const packageRef of packageRefs) {
71
+ version.includes.add(packageRef);
72
+ }
73
+ listItem.children.pop();
74
+ }
75
+ }
76
+ const firstPara = listItem.children[0]?.type === "paragraph" ? listItem.children[0] : void 0;
77
+ if (firstPara) {
78
+ visit(firstPara, "text", (textNode) => {
79
+ textNode.value = textNode.value.replace(/(^[0-9a-f]{7,}: | \[[0-9a-f]{7,}\]$)/, "");
80
+ });
81
+ const firstParaText = ToString(firstPara);
82
+ if (firstParaText === "Updated dependencies") continue;
83
+ const packageRef = parsePackageReference(firstParaText);
84
+ if (packageRef) {
85
+ version.includes.add(firstParaText);
86
+ continue;
87
+ }
88
+ version.changes[semverCategory].children.push(listItem);
89
+ }
90
+ }
91
+ return;
92
+ }
93
+ throw new Error(`Unexpected node: ${JSON.stringify(node)}`);
94
+ }
95
+ ast.children.forEach((node) => {
96
+ handleNode(node);
97
+ });
98
+ return changelog;
99
+ }
100
+ function parsePackageReference(str) {
101
+ const matches = str.match(/^([@/a-z0-9-]+)@([0-9.]+)$/);
102
+ if (!matches) return;
103
+ const [, packageName, version] = matches;
104
+ return { packageName, version };
105
+ }
106
+ export {
107
+ loadChangelog,
108
+ semverCategories
109
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Generates an export statement for a renderer component based on the provided component path and page type.
3
+ *
4
+ * @param comp - The path to the renderer component. Must be a non-undefined string.
5
+ * @param safePageType - The page type to use as the exported name.
6
+ * @returns An export statement string that re-exports the default export of the component under the given page type name.
7
+ * @throws {Error} If the `comp` parameter is undefined.
8
+ */
9
+ export declare function rendererComponentFilter(comp: string | undefined, safePageType: string): string;
10
+ /**
11
+ * Generates an export statement for a page content component based on the provided component path and page type.
12
+ *
13
+ * @param comp - The path to the component as a string. If undefined, an error is thrown.
14
+ * @param safePageType - The safe page type string used as the export alias.
15
+ * @returns An export statement string that re-exports the default export of the component under the given page type alias.
16
+ * @throws {Error} If the `comp` parameter is not provided.
17
+ */
18
+ export declare function pageContentComponentFilter(comp: string | undefined, safePageType: string): string;
@@ -0,0 +1,16 @@
1
+ function rendererComponentFilter(comp, safePageType) {
2
+ if (!comp) {
3
+ throw new Error(`Renderer Component path is required for page type: ${safePageType}`);
4
+ }
5
+ return `export { default as ${safePageType} } from '${comp}';`;
6
+ }
7
+ function pageContentComponentFilter(comp, safePageType) {
8
+ if (!comp) {
9
+ throw new Error(`Page Content Component path is required for page type: ${safePageType}`);
10
+ }
11
+ return `export { default as ${safePageType} } from '${comp}';`;
12
+ }
13
+ export {
14
+ pageContentComponentFilter,
15
+ rendererComponentFilter
16
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Converts a given string into a "safe" string by replacing all non-alphanumeric
3
+ * characters with underscores, trimming leading and trailing underscores, and converting
4
+ * the result to lowercase.
5
+ *
6
+ * @param string - The input string to be converted.
7
+ * @returns The sanitized, lowercase string with non-alphanumeric characters replaced by underscores.
8
+ */
9
+ export declare function convertToSafeString(string: string): string;
@@ -0,0 +1,6 @@
1
+ function convertToSafeString(string) {
2
+ return string.replace(/[^a-zA-Z0-9]/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
3
+ }
4
+ export {
5
+ convertToSafeString
6
+ };
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@withstudiocms/internal_helpers",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "Internal helper utilities for StudioCMS",
5
+ "author": {
6
+ "name": "withstudiocms",
7
+ "url": "https://studiocms.dev"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/withstudiocms/studiocms.git",
12
+ "directory": "packages/@withstudiocms/internal_helpers"
13
+ },
14
+ "contributors": [
15
+ "Adammatthiesen",
16
+ "jdtjenkins",
17
+ "dreyfus92",
18
+ "code.spirit"
19
+ ],
20
+ "license": "MIT",
21
+ "keywords": [
22
+ "astro-studiocms",
23
+ "cms",
24
+ "studiocms"
25
+ ],
26
+ "homepage": "https://studiocms.dev",
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "provenance": true
30
+ },
31
+ "sideEffects": false,
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "default": "./dist/index.js"
39
+ },
40
+ "./utils": {
41
+ "types": "./dist/utils/index.d.ts",
42
+ "default": "./dist/utils/index.js"
43
+ },
44
+ "./astro-integration": {
45
+ "types": "./dist/astro-integration/index.d.ts",
46
+ "default": "./dist/astro-integration/index.js"
47
+ },
48
+ "./pathGenerators": {
49
+ "types": "./dist/pathGenerators.d.ts",
50
+ "default": "./dist/pathGenerators.js"
51
+ },
52
+ "./urlGenFactory": {
53
+ "types": "./dist/urlGenFactory.d.ts",
54
+ "default": "./dist/urlGenFactory.js"
55
+ },
56
+ "./headConfigSchema": {
57
+ "types": "./dist/headConfigSchema.d.ts",
58
+ "default": "./dist/headConfigSchema.js"
59
+ }
60
+ },
61
+ "type": "module",
62
+ "dependencies": {
63
+ "astro-integration-kit": "^0.19.0",
64
+ "mdast-util-from-markdown": "^2.0.2",
65
+ "mdast-util-to-string": "^4.0.0",
66
+ "unist-util-visit": "^5.0.0"
67
+ },
68
+ "devDependencies": {
69
+ "@types/mdast": "^4.0.4",
70
+ "@types/node": "^22.0.0"
71
+ },
72
+ "peerDependencies": {
73
+ "astro": "^5.12.9"
74
+ },
75
+ "scripts": {
76
+ "build": "buildkit build 'src/**/*.{ts,astro,css,json,png}'",
77
+ "dev": "buildkit dev 'src/**/*.{ts,astro,css,json,png}'",
78
+ "test": "buildkit test 'test/**/*.test.js'",
79
+ "typecheck": "tspc -p tsconfig.tspc.json"
80
+ }
81
+ }