@studiocms/cfetch 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 StudioCMS
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,92 @@
1
+ # `@studiocms/cfetch`
2
+
3
+ This is an [Astro integration](https://docs.astro.build/en/guides/integrations-guide/) that provides a cacheable fetch function for Astro SSR
4
+
5
+ ## Usage
6
+
7
+ ### Prerequisites
8
+
9
+ - Using with an Astro SSR project, While you could import and use this in an Astro SSG (static) project, it would have no benefit as Astro Static pages are pre-rendered.
10
+
11
+ ### Installation
12
+
13
+ Install the integration **automatically** using the Astro CLI:
14
+
15
+ ```bash
16
+ pnpm astro add @studiocms/cfetch
17
+ ```
18
+
19
+ ```bash
20
+ npx astro add @studiocms/cfetch
21
+ ```
22
+
23
+ ```bash
24
+ yarn astro add @studiocms/cfetch
25
+ ```
26
+
27
+ Or install it **manually**:
28
+
29
+ 1. Install the required dependencies
30
+
31
+ ```bash
32
+ pnpm add @studiocms/cfetch
33
+ ```
34
+
35
+ ```bash
36
+ npm install @studiocms/cfetch
37
+ ```
38
+
39
+ ```bash
40
+ yarn add @studiocms/cfetch
41
+ ```
42
+
43
+ 2. Add the integration to your astro config
44
+
45
+ ```diff
46
+ +import cFetch from "@studiocms/cfetch";
47
+
48
+ export default defineConfig({
49
+ integrations: [
50
+ + cFetch(),
51
+ ],
52
+ });
53
+ ```
54
+
55
+ ### Usage
56
+
57
+ You can import the cachedFetch function anywhere you would use a normal `fetch` call. `cfetch` adapts the same default options as fetch,
58
+
59
+ ```astro
60
+ ---
61
+ import { cFetch } from 'cached:fetch';
62
+
63
+ const response = await cFetch(
64
+ 'https://example.com', // string | URL | Request
65
+ { /* Normal fetch init optional options here, method, mode, etc. */ },
66
+ { lifetime: "1h" } // Optional, allows changing the default lifetime of the cache
67
+ );
68
+
69
+ const html = await response.text();
70
+ ---
71
+ ```
72
+
73
+ If you are also wanting the other available metadata (such as `lastChecked` value which is the last time the cache was updated) then you can add the following prop to cached fetch, changing the shape of the data output to the following:
74
+
75
+ ```astro
76
+ ---
77
+ import { cFetch } from 'cached:fetch';
78
+
79
+ const { lastCheck, data: response } = await cFetch(
80
+ 'https://example.com', // string | URL | Request
81
+ { /* Normal fetch init optional options here, method, mode, etc. */ },
82
+ { lifetime: "1h" }, // Optional, allows changing the default lifetime of the cache
83
+ true
84
+ );
85
+
86
+ const html = await response.text();
87
+ ---
88
+ ```
89
+
90
+ ## Licensing
91
+
92
+ [MIT Licensed](https://github.com/withstudiocms/cfetch/blob/main/LICENSE).
@@ -0,0 +1,82 @@
1
+ declare module 'virtual:cfetch/config' {
2
+ /**
3
+ * Default configuration for the cache passed from the user.
4
+ *
5
+ * @property lifetime - Specifies the duration for which the cache is valid.
6
+ * The format should be a template literal string representing
7
+ * either minutes (`<number>m`) or hours (`<number>h`).
8
+ * For example: "5m" for 5 minutes or "2h" for 2 hours.
9
+ */
10
+ const defaultConfig: import('./types').CacheConfig;
11
+ export default defaultConfig;
12
+ }
13
+
14
+ declare module 'c:fetch' {
15
+ /**
16
+ * Represents the type of the global `fetch` function.
17
+ *
18
+ * This type is derived from the built-in `fetch` function, allowing you to
19
+ * use it as a reference for type-safe operations involving `fetch`.
20
+ */
21
+ type FetchType = typeof fetch;
22
+ /**
23
+ * Represents the input parameter type for the `FetchType` function.
24
+ * This type is derived from the first parameter of the `FetchType` function.
25
+ */
26
+ type Input = Parameters<FetchType>[0];
27
+ /**
28
+ * Represents the `init` parameter of the `fetch` function, which is the second parameter
29
+ * in the `FetchType` function signature. This type is used to configure the request,
30
+ * including options such as method, headers, body, and other settings.
31
+ */
32
+ type Init = Parameters<FetchType>[1];
33
+ /**
34
+ * Represents the structure of cached data.
35
+ *
36
+ * @property lastCheck - The date and time when the cache was last checked.
37
+ * @property data - The cached response data.
38
+ */
39
+ type CacheDataValue = { lastCheck: Date; data: Response };
40
+ /**
41
+ * Represents the configuration for caching.
42
+ *
43
+ * @property lifetime - Specifies the duration for which the cache is valid.
44
+ * The format should be a template literal string representing
45
+ * either minutes (`<number>m`) or hours (`<number>h`).
46
+ * For example: "5m" for 5 minutes or "2h" for 2 hours.
47
+ */
48
+ export interface CacheConfig {
49
+ /**
50
+ * Specifies the duration for which the cache is valid.
51
+ * The format should be a template literal string representing
52
+ * either minutes (`<number>m`) or hours (`<number>h`).
53
+ * For example: "5m" for 5 minutes or "2h" for 2 hours.
54
+ */
55
+ lifetime: `${number}m` | `${number}h`;
56
+ }
57
+ /**
58
+ * Fetches data with caching capabilities. If the data is not present in the cache
59
+ * or the cached data is older than the specified lifetime, it fetches new data
60
+ * and updates the cache. Otherwise, it returns the cached data.
61
+ *
62
+ * @param input - The input to the fetch function, typically a URL or Request object.
63
+ * @param init - An optional configuration object for the fetch request.
64
+ * @param cacheConfig - Partial configuration for the cache behavior. Defaults to `defaultConfig`.
65
+ * @param metadata - A boolean indicating whether to return the full cached object (including metadata)
66
+ * or just the data. Defaults to `false`.
67
+ * @returns The fetched or cached data. If `full` is `true`, returns an object containing
68
+ * both the data and metadata (e.g., `lastCheck`).
69
+ * @throws An error if fetching new data fails and no cached data is available.
70
+ */
71
+ export function cFetch(
72
+ input: Input,
73
+ init?: Init,
74
+ cacheConfig?: Partial<CacheConfig>
75
+ ): Promise<Response>;
76
+ export function cFetch(
77
+ input: Input,
78
+ init?: Init,
79
+ cacheConfig?: Partial<CacheConfig>,
80
+ metadata?: boolean
81
+ ): Promise<CacheDataValue>;
82
+ }
@@ -0,0 +1,8 @@
1
+ import type { CacheConfig } from './types.js';
2
+ /**
3
+ * Default cache configuration object.
4
+ *
5
+ * @property {string} lifetime - The duration for which the cache is valid.
6
+ * Accepts a string representation of time, e.g., '1h' for 1 hour.
7
+ */
8
+ export declare const defaultConfig: CacheConfig;
package/dist/consts.js ADDED
@@ -0,0 +1,6 @@
1
+ const defaultConfig = {
2
+ lifetime: "1h"
3
+ };
4
+ export {
5
+ defaultConfig
6
+ };
@@ -0,0 +1,36 @@
1
+ /// <reference types="./cache.d.ts" preserve="true" />
2
+ import type { AstroIntegration } from 'astro';
3
+ import type { CacheConfig } from './types.js';
4
+ /**
5
+ * Astro integration that allows you to have a cached fetch function in your Astro SSR project.
6
+ *
7
+ * This integration will add a virtual import for `cached:fetch` that you can use in your components.
8
+ *
9
+ * @returns {AstroIntegration} The integration object for Astro
10
+ * @see {@link https://github.com/withstudiocms/cfetch} for more details
11
+ * @example
12
+ * ```typescript
13
+ * import { defineConfig } from 'astro/config';
14
+ * import cFetch from '@studiocms/cfetch';
15
+ *
16
+ * export default defineConfig({
17
+ * integrations: [
18
+ * cFetch({
19
+ * lifetime: '1h', // OPTIONAL Cache lifetime, can be '<number>m' or '<number>h'
20
+ * })
21
+ * ],
22
+ * });
23
+ * ```
24
+ *
25
+ * then in your components you can use:
26
+ *
27
+ * ```typescript
28
+ * import { cFetch } from 'c:fetch';
29
+ *
30
+ * const response = await cFetch('https://example.com/api/data');
31
+ * const data = await response.json();
32
+ * // Use the data in your component
33
+ * ```
34
+ */
35
+ export declare function astroCache(opts?: CacheConfig): AstroIntegration;
36
+ export default astroCache;
package/dist/index.js ADDED
@@ -0,0 +1,29 @@
1
+ import { addVirtualImports, createResolver } from "./utils/integration.js";
2
+ import { defaultConfig } from "./consts.js";
3
+ function astroCache(opts) {
4
+ const name = "@studiocms/cfetch";
5
+ const { resolve } = createResolver(import.meta.url);
6
+ const options = {
7
+ ...defaultConfig,
8
+ ...opts
9
+ };
10
+ return {
11
+ name,
12
+ hooks: {
13
+ "astro:config:setup": (params) => {
14
+ addVirtualImports(params, {
15
+ name,
16
+ imports: {
17
+ "virtual:cfetch/config": `export default ${JSON.stringify(options)}`,
18
+ "c:fetch": `export * from '${resolve("./wrappers.js")}';`
19
+ }
20
+ });
21
+ }
22
+ }
23
+ };
24
+ }
25
+ var index_default = astroCache;
26
+ export {
27
+ astroCache,
28
+ index_default as default
29
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Represents the type of the global `fetch` function.
3
+ *
4
+ * This type is derived from the built-in `fetch` function, allowing you to
5
+ * use it as a reference for type-safe operations involving `fetch`.
6
+ */
7
+ export type FetchType = typeof fetch;
8
+ /**
9
+ * Represents the input parameter type for the `FetchType` function.
10
+ * This type is derived from the first parameter of the `FetchType` function.
11
+ */
12
+ export type Input = Parameters<FetchType>[0];
13
+ /**
14
+ * Represents the `init` parameter of the `fetch` function, which is the second parameter
15
+ * in the `FetchType` function signature. This type is used to configure the request,
16
+ * including options such as method, headers, body, and other settings.
17
+ */
18
+ export type Init = Parameters<FetchType>[1];
19
+ /**
20
+ * Represents the structure of cached data.
21
+ *
22
+ * @property lastCheck - The date and time when the cache was last checked.
23
+ * @property data - The cached response data.
24
+ */
25
+ export type CacheDataValue = {
26
+ lastCheck: Date;
27
+ data: Response;
28
+ };
29
+ /**
30
+ * Represents the configuration for caching.
31
+ *
32
+ * @property lifetime - Specifies the duration for which the cache is valid.
33
+ * The format should be a template literal string representing
34
+ * either minutes (`<number>m`) or hours (`<number>h`).
35
+ * For example: "5m" for 5 minutes or "2h" for 2 hours.
36
+ */
37
+ export interface CacheConfig {
38
+ /**
39
+ * Specifies the duration for which the cache is valid.
40
+ * The format should be a template literal string representing
41
+ * either minutes (`<number>m`) or hours (`<number>h`).
42
+ * For example: "5m" for 5 minutes or "2h" for 2 hours.
43
+ */
44
+ lifetime: `${number}m` | `${number}h`;
45
+ }
package/dist/types.js ADDED
File without changes
@@ -0,0 +1,143 @@
1
+ import type path from 'node:path';
2
+ import type { HookParameters } from 'astro';
3
+ import type { PluginOption } from 'vite';
4
+ export declare const isAbsolute: typeof path.isAbsolute;
5
+ export declare const dirname: typeof path.dirname;
6
+ /**
7
+ * Resolves a string path, resolving '.' and '.' segments and allowing paths above the root.
8
+ *
9
+ * @param path - The path to normalise.
10
+ * @param allowAboveRoot - Whether to allow the resulting path to be above the root directory.
11
+ * @returns the normalised path string.
12
+ */
13
+ export declare function normalizeString(path: string, allowAboveRoot: boolean): string;
14
+ export declare function normalizeWindowsPath(input?: string): string;
15
+ export declare const resolve: typeof path.resolve;
16
+ export type Hooks = Required<Astro.IntegrationHooks>;
17
+ /**
18
+ * Allows resolving paths relatively to the integration folder easily. Call it like this:
19
+ *
20
+ * @param {string} _base - The location you want to create relative references from. `import.meta.url` is usually what you'll want.
21
+ *
22
+ * @see https://astro-integration-kit.netlify.app/core/create-resolver/
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const { resolve } = createResolver(import.meta.url);
27
+ * const pluginPath = resolve("./plugin.ts");
28
+ * ```
29
+ *
30
+ * This way, you do not have to add your plugin to your package.json `exports`.
31
+ */
32
+ export declare const createResolver: (_base: string) => {
33
+ resolve: (...path: Array<string>) => string;
34
+ };
35
+ /**
36
+ * A utility to be used on an Astro hook.
37
+ *
38
+ * @see defineUtility
39
+ */
40
+ export type HookUtility<THook extends keyof Hooks, TArgs extends Array<any>, TReturn> = (params: HookParameters<THook>, ...args: TArgs) => TReturn;
41
+ /**
42
+ * Allows defining a type-safe function requiring all the params of a given hook.
43
+ * It uses currying to make TypeScript happy.
44
+ *
45
+ * @param {string} _hook
46
+ *
47
+ * @see https://astro-integration-kit.netlify.app/core/define-utility/
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const test = defineUtility("astro:config:setup")((params, foo: boolean) => {
52
+ * return "bar";
53
+ * });
54
+ * ```
55
+ */
56
+ export declare const defineUtility: <THook extends keyof Hooks>(_hook: THook) => <TArgs extends Array<any>, T>(fn: HookUtility<THook, TArgs, T>) => HookUtility<THook, TArgs, T>;
57
+ /**
58
+ * Checks for the existence of a Vite plugin inside the Astro config.
59
+ *
60
+ * @param {import("astro").HookParameters<"astro:config:setup">} params
61
+ * @param {Params} options
62
+ * @param {string | import("vite").PluginOption} options.plugin
63
+ *
64
+ * @see https://astro-integration-kit.netlify.app/utilities/has-vite-plugin/
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * hasVitePlugin(params, {
69
+ * plugin: "vite-plugin-my-integration",
70
+ * })
71
+ * ```
72
+ */
73
+ export declare const hasVitePlugin: HookUtility<"astro:config:setup", [{
74
+ plugin: string | PluginOption;
75
+ }], boolean>;
76
+ /**
77
+ * Adds a [vite plugin](https://vitejs.dev/guide/using-plugins) to the
78
+ * Astro config.
79
+ *
80
+ * @param {import("astro").HookParameters<"astro:config:setup">} params
81
+ * @param {object} options
82
+ * @param {import("vite").PluginOption} options.plugin
83
+ * @param {boolean} [options.warnDuplicated=true]
84
+ *
85
+ * @see https://astro-integration-kit.netlify.app/utilities/add-vite-plugin/
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * addVitePlugin(params, {
90
+ * plugin,
91
+ * warnDuplicated: true,
92
+ * })
93
+ * ```
94
+ */
95
+ export declare const addVitePlugin: HookUtility<"astro:config:setup", [{
96
+ plugin: PluginOption;
97
+ warnDuplicated?: boolean;
98
+ }], void>;
99
+ type VirtualImport = {
100
+ id: string;
101
+ content: string;
102
+ context?: 'server' | 'client' | undefined;
103
+ };
104
+ type Imports = Record<string, string> | Array<VirtualImport>;
105
+ /**
106
+ * Creates a Vite virtual module and updates the Astro config.
107
+ * Virtual imports are useful for passing things like config options, or data computed within the integration.
108
+ *
109
+ * @param {import("astro").HookParameters<"astro:config:setup">} params
110
+ * @param {object} options
111
+ * @param {string} options.name
112
+ * @param {Imports} options.imports
113
+ *
114
+ * @see https://astro-integration-kit.netlify.app/utilities/add-virtual-imports/
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * // my-integration/index.ts
119
+ * import { addVirtualImports } from "astro-integration-kit";
120
+ *
121
+ * addVirtualImports(params, {
122
+ * name: 'my-integration',
123
+ * imports: {
124
+ * 'virtual:my-integration/config': `export default ${ JSON.stringify({foo: "bar"}) }`,
125
+ * },
126
+ * });
127
+ * ```
128
+ *
129
+ * This is then readable anywhere else in your integration:
130
+ *
131
+ * ```ts
132
+ * // myIntegration/src/component/layout.astro
133
+ * import config from "virtual:my-integration/config";
134
+ *
135
+ * console.log(config.foo) // "bar"
136
+ * ```
137
+ */
138
+ export declare const addVirtualImports: HookUtility<"astro:config:setup", [{
139
+ name: string;
140
+ imports: Imports;
141
+ __enableCorePowerDoNotUseOrYouWillBeFired?: boolean;
142
+ }], void>;
143
+ export {};
@@ -0,0 +1,267 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { AstroError } from "astro/errors";
3
+ const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/;
4
+ const _DRIVE_LETTER_RE = /^[A-Za-z]:$/;
5
+ const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
6
+ const isAbsolute = (p) => _IS_ABSOLUTE_RE.test(p);
7
+ const dirname = (p) => {
8
+ const segments = normalizeWindowsPath(p).replace(/\/$/, "").split("/").slice(0, -1);
9
+ if (segments.length === 1 && _DRIVE_LETTER_RE.test(segments[0])) {
10
+ segments[0] += "/";
11
+ }
12
+ return segments.join("/") || (isAbsolute(p) ? "/" : ".");
13
+ };
14
+ function cwd() {
15
+ if (typeof process !== "undefined" && typeof process.cwd === "function") {
16
+ return process.cwd().replace(/\\/g, "/");
17
+ }
18
+ return "/";
19
+ }
20
+ function normalizeString(path, allowAboveRoot) {
21
+ let res = "";
22
+ let lastSegmentLength = 0;
23
+ let lastSlash = -1;
24
+ let dots = 0;
25
+ let char = null;
26
+ for (let index = 0; index <= path.length; ++index) {
27
+ if (index < path.length) {
28
+ char = path[index];
29
+ } else if (char === "/") {
30
+ break;
31
+ } else {
32
+ char = "/";
33
+ }
34
+ if (char === "/") {
35
+ if (lastSlash === index - 1 || dots === 1) {
36
+ } else if (dots === 2) {
37
+ if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") {
38
+ if (res.length > 2) {
39
+ const lastSlashIndex = res.lastIndexOf("/");
40
+ if (lastSlashIndex === -1) {
41
+ res = "";
42
+ lastSegmentLength = 0;
43
+ } else {
44
+ res = res.slice(0, lastSlashIndex);
45
+ lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
46
+ }
47
+ lastSlash = index;
48
+ dots = 0;
49
+ continue;
50
+ }
51
+ if (res.length > 0) {
52
+ res = "";
53
+ lastSegmentLength = 0;
54
+ lastSlash = index;
55
+ dots = 0;
56
+ continue;
57
+ }
58
+ }
59
+ if (allowAboveRoot) {
60
+ res += res.length > 0 ? "/.." : "..";
61
+ lastSegmentLength = 2;
62
+ }
63
+ } else {
64
+ if (res.length > 0) {
65
+ res += `/${path.slice(lastSlash + 1, index)}`;
66
+ } else {
67
+ res = path.slice(lastSlash + 1, index);
68
+ }
69
+ lastSegmentLength = index - lastSlash - 1;
70
+ }
71
+ lastSlash = index;
72
+ dots = 0;
73
+ } else if (char === "." && dots !== -1) {
74
+ ++dots;
75
+ } else {
76
+ dots = -1;
77
+ }
78
+ }
79
+ return res;
80
+ }
81
+ function normalizeWindowsPath(input = "") {
82
+ if (!input) {
83
+ return input;
84
+ }
85
+ return input.replace(/\\/g, "/").replace(_DRIVE_LETTER_START_RE, (r) => r.toUpperCase());
86
+ }
87
+ const resolve = (...arguments_) => {
88
+ arguments_ = arguments_.map((argument) => normalizeWindowsPath(argument));
89
+ let resolvedPath = "";
90
+ let resolvedAbsolute = false;
91
+ for (let index = arguments_.length - 1; index >= -1 && !resolvedAbsolute; index--) {
92
+ const path = index >= 0 ? arguments_[index] : cwd();
93
+ if (!path || path.length === 0) {
94
+ continue;
95
+ }
96
+ resolvedPath = `${path}/${resolvedPath}`;
97
+ resolvedAbsolute = isAbsolute(path);
98
+ }
99
+ resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute);
100
+ if (resolvedAbsolute && !isAbsolute(resolvedPath)) {
101
+ return `/${resolvedPath}`;
102
+ }
103
+ return resolvedPath.length > 0 ? resolvedPath : ".";
104
+ };
105
+ const createResolver = (_base) => {
106
+ let base = _base;
107
+ if (base.startsWith("file://")) {
108
+ base = dirname(fileURLToPath(base));
109
+ }
110
+ return {
111
+ resolve: (...path) => resolve(base, ...path)
112
+ };
113
+ };
114
+ const defineUtility = (_hook) => (
115
+ /**
116
+ * The function itself
117
+ * @param {Function} fn;
118
+ */
119
+ // biome-ignore lint/suspicious/noExplicitAny: <explanation>
120
+ (fn) => fn
121
+ );
122
+ function getPluginNames(plugins) {
123
+ const names = [];
124
+ if (plugins) {
125
+ for (const plugin of plugins) {
126
+ if (!plugin) continue;
127
+ if (Array.isArray(plugin)) {
128
+ names.push(...getPluginNames(plugin));
129
+ continue;
130
+ }
131
+ if (plugin instanceof Promise) {
132
+ continue;
133
+ }
134
+ names.push(plugin.name);
135
+ }
136
+ }
137
+ return names;
138
+ }
139
+ const hasVitePlugin = defineUtility("astro:config:setup")(
140
+ ({ config }, {
141
+ plugin
142
+ }) => {
143
+ if (!plugin || plugin instanceof Promise) return false;
144
+ const currentPlugins = new Set(getPluginNames(config?.vite?.plugins));
145
+ const plugins = /* @__PURE__ */ new Set();
146
+ if (typeof plugin === "string") {
147
+ plugins.add(plugin);
148
+ }
149
+ if (typeof plugin === "object") {
150
+ if (Array.isArray(plugin)) {
151
+ const names = new Set(
152
+ getPluginNames(plugin)
153
+ );
154
+ for (const name of names) plugins.add(name);
155
+ } else {
156
+ plugins.add(plugin.name);
157
+ }
158
+ }
159
+ return [...plugins].some((name) => currentPlugins.has(name));
160
+ }
161
+ );
162
+ const addVitePlugin = defineUtility("astro:config:setup")(
163
+ (params, {
164
+ plugin,
165
+ warnDuplicated = true
166
+ }) => {
167
+ const { updateConfig, logger } = params;
168
+ if (warnDuplicated && hasVitePlugin(params, { plugin })) {
169
+ logger.warn(
170
+ `The Vite plugin "${plugin.name}" is already present in your Vite configuration, this plugin may not behave correctly.`
171
+ );
172
+ }
173
+ updateConfig({
174
+ vite: {
175
+ plugins: [plugin]
176
+ }
177
+ });
178
+ }
179
+ );
180
+ const incrementPluginName = (name) => {
181
+ let count = 1;
182
+ return `${name.replace(/-(\d+)$/, (_, c) => {
183
+ count = Number.parseInt(c) + 1;
184
+ return "";
185
+ })}-${count}`;
186
+ };
187
+ const resolveVirtualModuleId = (id) => {
188
+ return `\0${id}`;
189
+ };
190
+ const createVirtualModule = (name, _imports, bypassCoreValidation) => {
191
+ const imports = Array.isArray(_imports) ? _imports : Object.entries(_imports).map(([id, content]) => ({
192
+ id,
193
+ content,
194
+ context: void 0
195
+ }));
196
+ const duplicatedImports = {};
197
+ for (const { id, context } of imports) {
198
+ duplicatedImports[id] ??= [];
199
+ duplicatedImports[id]?.push(...context === void 0 ? ["server", "client"] : [context]);
200
+ }
201
+ for (const [id, contexts] of Object.entries(duplicatedImports)) {
202
+ if (contexts.length !== [...new Set(contexts)].length) {
203
+ throw new AstroError(
204
+ `Virtual import with id "${id}" has been registered several times with conflicting contexts.`
205
+ );
206
+ }
207
+ }
208
+ const resolutionMap = Object.fromEntries(
209
+ imports.map(({ id }) => {
210
+ if (!bypassCoreValidation && id.startsWith("astro:")) {
211
+ throw new AstroError(
212
+ `Virtual import name prefix can't be "astro:" (for "${id}") because it's reserved for Astro core.`
213
+ );
214
+ }
215
+ return [resolveVirtualModuleId(id), id];
216
+ })
217
+ );
218
+ return {
219
+ name,
220
+ resolveId(id) {
221
+ if (imports.find((_import) => _import.id === id)) {
222
+ return resolveVirtualModuleId(id);
223
+ }
224
+ return;
225
+ },
226
+ load(id, options) {
227
+ const resolution = resolutionMap[id];
228
+ if (resolution) {
229
+ const context = options?.ssr ? "server" : "client";
230
+ const data = imports.find(
231
+ (_import) => _import.id === resolution && (_import.context === void 0 || _import.context === context)
232
+ );
233
+ if (data) {
234
+ return data.content;
235
+ }
236
+ }
237
+ return;
238
+ }
239
+ };
240
+ };
241
+ const addVirtualImports = defineUtility("astro:config:setup")(
242
+ (params, {
243
+ name,
244
+ imports,
245
+ __enableCorePowerDoNotUseOrYouWillBeFired = false
246
+ }) => {
247
+ let pluginName = `vite-plugin-${name}`;
248
+ while (hasVitePlugin(params, { plugin: pluginName }))
249
+ pluginName = incrementPluginName(pluginName);
250
+ addVitePlugin(params, {
251
+ warnDuplicated: false,
252
+ plugin: createVirtualModule(pluginName, imports, __enableCorePowerDoNotUseOrYouWillBeFired)
253
+ });
254
+ }
255
+ );
256
+ export {
257
+ addVirtualImports,
258
+ addVitePlugin,
259
+ createResolver,
260
+ defineUtility,
261
+ dirname,
262
+ hasVitePlugin,
263
+ isAbsolute,
264
+ normalizeString,
265
+ normalizeWindowsPath,
266
+ resolve
267
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Determines whether a given date is older than a specified lifetime.
3
+ *
4
+ * @param date - The date to compare against the current time.
5
+ * @param lifetime - The lifetime duration in the format of `${number}m` for minutes
6
+ * or `${number}h` for hours (e.g., "30m" or "2h").
7
+ * @returns `true` if the given date is older than the specified lifetime, otherwise `false`.
8
+ */
9
+ export default function isOlderThan(date: Date, lifetime: `${number}m` | `${number}h`): boolean;
@@ -0,0 +1,13 @@
1
+ function isOlderThan(date, lifetime) {
2
+ const now = /* @__PURE__ */ new Date();
3
+ let milliseconds = 0;
4
+ if (lifetime.endsWith("m")) {
5
+ milliseconds = Number.parseInt(lifetime) * 60 * 1e3;
6
+ } else if (lifetime.endsWith("h")) {
7
+ milliseconds = Number.parseInt(lifetime) * 60 * 60 * 1e3;
8
+ }
9
+ return date < new Date(now.getTime() - milliseconds);
10
+ }
11
+ export {
12
+ isOlderThan as default
13
+ };
@@ -0,0 +1,8 @@
1
+ import type { CacheConfig, CacheDataValue, Init, Input } from './types.js';
2
+ export type { CacheConfig };
3
+ /**
4
+ * Exported for tests
5
+ */
6
+ export declare const cachedData: Map<string, CacheDataValue>;
7
+ export declare function cFetch(input: Input, init?: Init, cacheConfig?: Partial<CacheConfig>): Promise<Response>;
8
+ export declare function cFetch(input: Input, init?: Init, cacheConfig?: Partial<CacheConfig>, metadata?: boolean): Promise<CacheDataValue>;
@@ -0,0 +1,39 @@
1
+ import isOlderThan from "./utils/isOlderThan.js";
2
+ import { defaultConfig as _config } from "./consts.js";
3
+ const defaultConfig = await import("virtual:cfetch/config").then((mod) => {
4
+ return mod.default;
5
+ }).catch(() => {
6
+ return _config;
7
+ });
8
+ const cachedData = /* @__PURE__ */ new Map();
9
+ async function cFetch(input, init = void 0, cacheConfig = defaultConfig, metadata = false) {
10
+ if (init?.method && init.method !== "GET") {
11
+ console.warn(
12
+ "Warning: cFetch is designed for GET requests. Using it with other methods will not cache the response."
13
+ );
14
+ const newResponse = fetch(input, init);
15
+ return metadata ? { lastCheck: /* @__PURE__ */ new Date(), data: newResponse } : newResponse;
16
+ }
17
+ const { lifetime } = {
18
+ ...defaultConfig,
19
+ ...cacheConfig
20
+ };
21
+ const storedData = cachedData.get(input.toString());
22
+ if (!storedData || isOlderThan(storedData.lastCheck, lifetime)) {
23
+ const newResponse = await fetch(input, init);
24
+ if (!newResponse.ok) {
25
+ if (!storedData) {
26
+ throw new Error("Failed to retrieve cached data, and failed to fetch new data");
27
+ }
28
+ return metadata ? storedData : storedData.data;
29
+ }
30
+ const newCachedData = { lastCheck: /* @__PURE__ */ new Date(), data: newResponse };
31
+ cachedData.set(input.toString(), newCachedData);
32
+ return metadata ? newCachedData : newResponse;
33
+ }
34
+ return metadata ? storedData : storedData.data;
35
+ }
36
+ export {
37
+ cFetch,
38
+ cachedData
39
+ };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@studiocms/cfetch",
3
+ "version": "0.0.1",
4
+ "description": "Astro integration that allows you to have a cached fetch function in your Astro SSR project.",
5
+ "license": "MIT",
6
+ "homepage": "https://studiocms.dev",
7
+ "sideEffects": false,
8
+ "type": "module",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/withstudiocms/cfetch.git",
12
+ "directory": "packages/cfetch"
13
+ },
14
+ "author": {
15
+ "name": "Adam Matthiesen | Jacob Jenkins | Paul Valladares",
16
+ "url": "https://studiocms.dev"
17
+ },
18
+ "contributors": [
19
+ "Adammatthiesen",
20
+ "jdtjenkins",
21
+ "dreyfus92",
22
+ "code.spirit"
23
+ ],
24
+ "keywords": [
25
+ "astro",
26
+ "astrocms",
27
+ "astrodb",
28
+ "astrostudio",
29
+ "astro-integration",
30
+ "astro-studio",
31
+ "astro-studiocms",
32
+ "studiocms",
33
+ "withastro"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "provenance": true
38
+ },
39
+ "files": [
40
+ "dist"
41
+ ],
42
+ "exports": {
43
+ ".": {
44
+ "types": "./dist/index.d.ts",
45
+ "default": "./dist/index.js"
46
+ },
47
+ "./types": {
48
+ "types": "./dist/types.d.ts",
49
+ "default": "./dist/types.js"
50
+ },
51
+ "./v/types": "./dist/cache.d.ts"
52
+ },
53
+ "devDependencies": {
54
+ "@inox-tools/astro-tests": "^0.5.1",
55
+ "@types/node": "^22.0.0",
56
+ "jest-extended": "^4.0.2"
57
+ },
58
+ "peerDependencies": {
59
+ "astro": "^5.5",
60
+ "vite": "^6.2.0"
61
+ },
62
+ "scripts": {
63
+ "build": "buildkit build 'src/**/*.{ts,astro,css,js}'",
64
+ "dev": "buildkit dev 'src/**/*.{ts,astro,css,js}'"
65
+ }
66
+ }