@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 +21 -0
- package/README.md +92 -0
- package/dist/cache.d.ts +82 -0
- package/dist/consts.d.ts +8 -0
- package/dist/consts.js +6 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +29 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.js +0 -0
- package/dist/utils/integration.d.ts +143 -0
- package/dist/utils/integration.js +267 -0
- package/dist/utils/isOlderThan.d.ts +9 -0
- package/dist/utils/isOlderThan.js +13 -0
- package/dist/wrappers.d.ts +8 -0
- package/dist/wrappers.js +39 -0
- package/package.json +66 -0
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).
|
package/dist/cache.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/consts.d.ts
ADDED
|
@@ -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
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -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>;
|
package/dist/wrappers.js
ADDED
|
@@ -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
|
+
}
|