nuxt-safe-runtime-config 0.0.11 → 0.0.13
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/README.md +107 -35
- package/dist/eslint.d.cts +2 -0
- package/dist/eslint.d.mts +30 -0
- package/dist/eslint.d.ts +30 -0
- package/dist/eslint.mjs +38 -0
- package/dist/module.d.cts +2 -0
- package/dist/module.d.mts +26 -2
- package/dist/module.d.ts +34 -0
- package/dist/module.json +4 -1
- package/dist/module.mjs +233 -19
- package/dist/types.d.mts +12 -2
- package/package.json +55 -43
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Validate Nuxt runtime config at build time using <b>Zod</b>, <b>Valibot</b>, <b>
|
|
|
18
18
|
<img src="https://img.shields.io/github/license/onmax/nuxt-safe-runtime-config.svg" alt="License" />
|
|
19
19
|
</a>
|
|
20
20
|
<a href="https://nuxt.com">
|
|
21
|
-
<img src="https://img.shields.io/badge/Nuxt-3.0
|
|
21
|
+
<img src="https://img.shields.io/badge/Nuxt-3.0+-00DC82.svg" alt="Nuxt" />
|
|
22
22
|
</a>
|
|
23
23
|
|
|
24
24
|
<p align="center">
|
|
@@ -30,15 +30,15 @@ Validate Nuxt runtime config at build time using <b>Zod</b>, <b>Valibot</b>, <b>
|
|
|
30
30
|
|
|
31
31
|
## Features
|
|
32
32
|
|
|
33
|
-
- 🔒
|
|
34
|
-
- 🚀
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
33
|
+
- 🔒 **Build-time validation** with Zod, Valibot, ArkType, or any [Standard Schema](https://standardschema.dev/) library
|
|
34
|
+
- 🚀 **Runtime validation** (opt-in) validates config when the server starts
|
|
35
|
+
- ✨ **Auto-generated types** — `useSafeRuntimeConfig()` is fully typed without manual generics
|
|
36
|
+
- 🛠 **ESLint plugin** warns when using `useRuntimeConfig()` instead of the type-safe composable
|
|
37
|
+
- ⚡ **Zero runtime overhead** by default — validation happens at build time only
|
|
38
38
|
|
|
39
39
|
## Quick Setup
|
|
40
40
|
|
|
41
|
-
Install the module
|
|
41
|
+
Install the module:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
44
|
npx nuxi module add nuxt-safe-runtime-config
|
|
@@ -46,15 +46,9 @@ npx nuxi module add nuxt-safe-runtime-config
|
|
|
46
46
|
|
|
47
47
|
## Usage
|
|
48
48
|
|
|
49
|
-
1.
|
|
49
|
+
### 1. Define your schema
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
export default defineNuxtConfig({
|
|
53
|
-
modules: ['nuxt-safe-runtime-config']
|
|
54
|
-
})
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
2. Define your runtime config schema using **Valibot**, **Zod**, **ArkType**, or any other Standard Schema compatible library:
|
|
51
|
+
Use Zod, Valibot, ArkType, or any Standard Schema compatible library:
|
|
58
52
|
|
|
59
53
|
<details>
|
|
60
54
|
<summary>With Valibot</summary>
|
|
@@ -113,13 +107,12 @@ const runtimeConfigSchema = type({
|
|
|
113
107
|
|
|
114
108
|
</details>
|
|
115
109
|
|
|
116
|
-
|
|
110
|
+
### 2. Configure in nuxt.config.ts
|
|
117
111
|
|
|
118
112
|
```typescript
|
|
119
113
|
export default defineNuxtConfig({
|
|
120
114
|
modules: ['nuxt-safe-runtime-config'],
|
|
121
115
|
|
|
122
|
-
// Your regular runtime config
|
|
123
116
|
runtimeConfig: {
|
|
124
117
|
databaseUrl: process.env.DATABASE_URL || 'postgresql://localhost:5432/mydb',
|
|
125
118
|
secretKey: process.env.SECRET_KEY || 'default-secret-key',
|
|
@@ -130,45 +123,124 @@ export default defineNuxtConfig({
|
|
|
130
123
|
},
|
|
131
124
|
},
|
|
132
125
|
|
|
133
|
-
// Add your schema for validation
|
|
134
126
|
safeRuntimeConfig: {
|
|
135
127
|
$schema: runtimeConfigSchema,
|
|
136
128
|
},
|
|
137
129
|
})
|
|
138
130
|
```
|
|
139
131
|
|
|
140
|
-
|
|
132
|
+
### 3. Use the type-safe composable
|
|
133
|
+
|
|
134
|
+
Access your validated config with full type safety — types are auto-generated from your schema:
|
|
135
|
+
|
|
136
|
+
```vue
|
|
137
|
+
<script setup lang="ts">
|
|
138
|
+
const config = useSafeRuntimeConfig()
|
|
139
|
+
// config.public.apiBase is typed as string
|
|
140
|
+
// config.secretKey is typed as string
|
|
141
|
+
</script>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Configuration Options
|
|
145
|
+
|
|
146
|
+
| Option | Type | Default | Description |
|
|
147
|
+
| ------------------- | ------------------------------- | ----------------- | ------------------------------------------ |
|
|
148
|
+
| `$schema` | `StandardSchemaV1` | — | Your validation schema (required) |
|
|
149
|
+
| `validateAtBuild` | `boolean` | `true` | Validate during dev/build |
|
|
150
|
+
| `validateAtRuntime` | `boolean` | `false` | Validate when server starts |
|
|
151
|
+
| `onBuildError` | `'throw' \| 'warn' \| 'ignore'` | `'throw'` | How to handle build validation errors |
|
|
152
|
+
| `onRuntimeError` | `'throw' \| 'warn' \| 'ignore'` | `'throw'` | How to handle runtime validation errors |
|
|
153
|
+
| `logSuccess` | `boolean` | `true` | Log successful validation |
|
|
154
|
+
| `logFallback` | `boolean` | `true` | Log when using JSON Schema fallback |
|
|
155
|
+
| `jsonSchemaTarget` | `string` | `'draft-2020-12'` | JSON Schema version for runtime validation |
|
|
156
|
+
|
|
157
|
+
## Runtime Validation
|
|
158
|
+
|
|
159
|
+
By default, validation only runs at build time. Enable runtime validation to catch environment variable issues when the server starts:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
export default defineNuxtConfig({
|
|
163
|
+
safeRuntimeConfig: {
|
|
164
|
+
$schema: runtimeConfigSchema,
|
|
165
|
+
validateAtRuntime: true,
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Runtime validation uses [@cfworker/json-schema](https://github.com/cfworker/cfworker/tree/main/packages/json-schema) to validate the config after environment variables are merged. This lightweight validator (~8KB) works on all runtimes including edge (Cloudflare Workers, Vercel Edge, Netlify Edge). It catches issues like:
|
|
171
|
+
|
|
172
|
+
- Environment variables with wrong types (e.g., `NUXT_PORT=abc` when expecting a number)
|
|
173
|
+
- Missing required environment variables in production
|
|
174
|
+
- Invalid values that pass build-time checks but fail at runtime
|
|
175
|
+
|
|
176
|
+
## ESLint Integration
|
|
177
|
+
|
|
178
|
+
The module includes an ESLint plugin that warns when using `useRuntimeConfig()` instead of `useSafeRuntimeConfig()`.
|
|
179
|
+
|
|
180
|
+
### With @nuxt/eslint (Automatic)
|
|
141
181
|
|
|
142
|
-
|
|
143
|
-
- `nuxi build`
|
|
144
|
-
- `nuxi generate`
|
|
182
|
+
If you use [@nuxt/eslint](https://eslint.nuxt.com), the rule is auto-registered. No configuration needed.
|
|
145
183
|
|
|
146
|
-
|
|
184
|
+
### Manual Setup
|
|
147
185
|
|
|
148
|
-
|
|
186
|
+
Add to your `eslint.config.mjs`:
|
|
149
187
|
|
|
150
|
-
|
|
188
|
+
```javascript
|
|
189
|
+
import safeRuntimeConfig from 'nuxt-safe-runtime-config/eslint'
|
|
190
|
+
|
|
191
|
+
export default [
|
|
192
|
+
safeRuntimeConfig.configs.recommended,
|
|
193
|
+
// ... your other configs
|
|
194
|
+
]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Or configure manually:
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
import safeRuntimeConfig from 'nuxt-safe-runtime-config/eslint'
|
|
201
|
+
|
|
202
|
+
export default [
|
|
203
|
+
{
|
|
204
|
+
plugins: { 'safe-runtime-config': safeRuntimeConfig },
|
|
205
|
+
rules: { 'safe-runtime-config/prefer-safe-runtime-config': 'warn' },
|
|
206
|
+
},
|
|
207
|
+
]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The rule includes auto-fix support — run `eslint --fix` to automatically replace `useRuntimeConfig()` calls.
|
|
211
|
+
|
|
212
|
+
## Type Safety
|
|
213
|
+
|
|
214
|
+
Types are auto-generated at build time from your schema's JSON Schema representation. The `useSafeRuntimeConfig()` composable returns a fully typed object — no manual generics needed:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const config = useSafeRuntimeConfig()
|
|
218
|
+
// config is fully typed based on your schema
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Generated types are stored in `.nuxt/types/safe-runtime-config.d.ts` and automatically included in your project.
|
|
151
222
|
|
|
152
223
|
## Error Messages
|
|
153
224
|
|
|
154
|
-
When validation fails, you
|
|
225
|
+
When validation fails, you see detailed error messages:
|
|
155
226
|
|
|
156
227
|
```
|
|
157
|
-
|
|
158
|
-
1. databaseUrl:
|
|
159
|
-
2. public.apiBase: Expected string
|
|
160
|
-
3. port: Expected number
|
|
228
|
+
[safe-runtime-config] Validation failed!
|
|
229
|
+
1. databaseUrl: Invalid type: Expected string but received undefined
|
|
230
|
+
2. public.apiBase: Invalid type: Expected string but received undefined
|
|
231
|
+
3. port: Invalid type: Expected number but received string
|
|
161
232
|
```
|
|
162
233
|
|
|
163
|
-
The module
|
|
234
|
+
The module stops the build process until all validation errors are resolved.
|
|
164
235
|
|
|
165
236
|
## Why This Module?
|
|
166
237
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
This module focuses specifically on **runtime config validation** using the Standard Schema specification, allowing you to use your preferred validation library (Valibot, Zod, ArkType, etc.).
|
|
238
|
+
Nuxt's built-in schema validation is designed for module authors and broader configuration. This module focuses specifically on **runtime config validation** using Standard Schema, allowing you to:
|
|
170
239
|
|
|
171
|
-
|
|
240
|
+
- Use your preferred validation library (Valibot, Zod, ArkType)
|
|
241
|
+
- Catch configuration errors at build time
|
|
242
|
+
- Optionally validate at runtime for environment variable issues
|
|
243
|
+
- Get full type safety in your components
|
|
172
244
|
|
|
173
245
|
## Contribution
|
|
174
246
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts-eslint';
|
|
2
|
+
|
|
3
|
+
declare const plugin: {
|
|
4
|
+
meta: {
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
rules: {
|
|
8
|
+
'prefer-safe-runtime-config': _typescript_eslint_utils_ts_eslint.RuleModule<"preferSafe", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
declare const configs: {
|
|
13
|
+
recommended: {
|
|
14
|
+
plugins: {
|
|
15
|
+
'safe-runtime-config': {
|
|
16
|
+
meta: {
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
rules: {
|
|
20
|
+
'prefer-safe-runtime-config': _typescript_eslint_utils_ts_eslint.RuleModule<"preferSafe", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
rules: {
|
|
25
|
+
'safe-runtime-config/prefer-safe-runtime-config': string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export { configs, plugin as default };
|
package/dist/eslint.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts-eslint';
|
|
2
|
+
|
|
3
|
+
declare const plugin: {
|
|
4
|
+
meta: {
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
rules: {
|
|
8
|
+
'prefer-safe-runtime-config': _typescript_eslint_utils_ts_eslint.RuleModule<"preferSafe", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
declare const configs: {
|
|
13
|
+
recommended: {
|
|
14
|
+
plugins: {
|
|
15
|
+
'safe-runtime-config': {
|
|
16
|
+
meta: {
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
rules: {
|
|
20
|
+
'prefer-safe-runtime-config': _typescript_eslint_utils_ts_eslint.RuleModule<"preferSafe", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
rules: {
|
|
25
|
+
'safe-runtime-config/prefer-safe-runtime-config': string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export { configs, plugin as default };
|
package/dist/eslint.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
|
|
3
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/onmax/nuxt-safe-runtime-config#${name}`);
|
|
4
|
+
const preferSafeRuntimeConfig = createRule({
|
|
5
|
+
name: "prefer-safe-runtime-config",
|
|
6
|
+
meta: {
|
|
7
|
+
type: "suggestion",
|
|
8
|
+
docs: { description: "Prefer useSafeRuntimeConfig() over useRuntimeConfig()" },
|
|
9
|
+
schema: [],
|
|
10
|
+
messages: { preferSafe: "Use useSafeRuntimeConfig() for type-safe runtime config" },
|
|
11
|
+
fixable: "code"
|
|
12
|
+
},
|
|
13
|
+
defaultOptions: [],
|
|
14
|
+
create: (context) => ({
|
|
15
|
+
CallExpression(node) {
|
|
16
|
+
if (node.callee.type === "Identifier" && node.callee.name === "useRuntimeConfig") {
|
|
17
|
+
context.report({
|
|
18
|
+
node,
|
|
19
|
+
messageId: "preferSafe",
|
|
20
|
+
fix: (fixer) => fixer.replaceText(node.callee, "useSafeRuntimeConfig")
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const plugin = {
|
|
28
|
+
meta: { name: "nuxt-safe-runtime-config" },
|
|
29
|
+
rules: { "prefer-safe-runtime-config": preferSafeRuntimeConfig }
|
|
30
|
+
};
|
|
31
|
+
const configs = {
|
|
32
|
+
recommended: {
|
|
33
|
+
plugins: { "safe-runtime-config": plugin },
|
|
34
|
+
rules: { "safe-runtime-config/prefer-safe-runtime-config": "warn" }
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export { configs, plugin as default };
|
package/dist/module.d.mts
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
-
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import { StandardSchemaV1, StandardJSONSchemaV1 } from '@standard-schema/spec';
|
|
3
3
|
|
|
4
|
+
type ErrorBehavior = 'throw' | 'warn' | 'ignore';
|
|
4
5
|
interface ModuleOptions {
|
|
5
6
|
$schema: StandardSchemaV1;
|
|
7
|
+
validateAtBuild: boolean;
|
|
8
|
+
validateAtRuntime: boolean;
|
|
9
|
+
jsonSchemaTarget?: StandardJSONSchemaV1.Target;
|
|
10
|
+
onBuildError?: ErrorBehavior;
|
|
11
|
+
onRuntimeError?: ErrorBehavior;
|
|
12
|
+
logSuccess?: boolean;
|
|
13
|
+
logFallback?: boolean;
|
|
6
14
|
}
|
|
7
15
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
8
16
|
|
|
17
|
+
declare module '@nuxt/schema' {
|
|
18
|
+
interface NuxtHooks {
|
|
19
|
+
'eslint:config:addons': (addons: Array<{
|
|
20
|
+
name: string;
|
|
21
|
+
getConfigs: () => {
|
|
22
|
+
imports?: Array<{
|
|
23
|
+
from: string;
|
|
24
|
+
name: string;
|
|
25
|
+
as: string;
|
|
26
|
+
}>;
|
|
27
|
+
configs?: string[];
|
|
28
|
+
};
|
|
29
|
+
}>) => void;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
9
33
|
export { _default as default };
|
|
10
|
-
export type { ModuleOptions };
|
|
34
|
+
export type { ErrorBehavior, ModuleOptions };
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
import { StandardSchemaV1, StandardJSONSchemaV1 } from '@standard-schema/spec';
|
|
3
|
+
|
|
4
|
+
type ErrorBehavior = 'throw' | 'warn' | 'ignore';
|
|
5
|
+
interface ModuleOptions {
|
|
6
|
+
$schema: StandardSchemaV1;
|
|
7
|
+
validateAtBuild: boolean;
|
|
8
|
+
validateAtRuntime: boolean;
|
|
9
|
+
jsonSchemaTarget?: StandardJSONSchemaV1.Target;
|
|
10
|
+
onBuildError?: ErrorBehavior;
|
|
11
|
+
onRuntimeError?: ErrorBehavior;
|
|
12
|
+
logSuccess?: boolean;
|
|
13
|
+
logFallback?: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
16
|
+
|
|
17
|
+
declare module '@nuxt/schema' {
|
|
18
|
+
interface NuxtHooks {
|
|
19
|
+
'eslint:config:addons': (addons: Array<{
|
|
20
|
+
name: string;
|
|
21
|
+
getConfigs: () => {
|
|
22
|
+
imports?: Array<{
|
|
23
|
+
from: string;
|
|
24
|
+
name: string;
|
|
25
|
+
as: string;
|
|
26
|
+
}>;
|
|
27
|
+
configs?: string[];
|
|
28
|
+
};
|
|
29
|
+
}>) => void;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { _default as default };
|
|
34
|
+
export type { ErrorBehavior, ModuleOptions };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,41 +1,255 @@
|
|
|
1
|
-
import { defineNuxtModule } from '@nuxt/kit';
|
|
1
|
+
import { useLogger, defineNuxtModule, createResolver, addTypeTemplate, addTemplate, addImportsDir } from '@nuxt/kit';
|
|
2
|
+
import { toJsonSchema } from '@standard-community/standard-json';
|
|
2
3
|
|
|
4
|
+
function jsonSchemaToTs(schema, indent = 0) {
|
|
5
|
+
const spaces = " ".repeat(indent);
|
|
6
|
+
if ("$ref" in schema) {
|
|
7
|
+
return "unknown";
|
|
8
|
+
}
|
|
9
|
+
if ("anyOf" in schema && Array.isArray(schema.anyOf)) {
|
|
10
|
+
const types = schema.anyOf.map((s) => jsonSchemaToTs(s, indent));
|
|
11
|
+
return types.join(" | ");
|
|
12
|
+
}
|
|
13
|
+
if ("oneOf" in schema && Array.isArray(schema.oneOf)) {
|
|
14
|
+
const types = schema.oneOf.map((s) => jsonSchemaToTs(s, indent));
|
|
15
|
+
return types.join(" | ");
|
|
16
|
+
}
|
|
17
|
+
if ("allOf" in schema && Array.isArray(schema.allOf)) {
|
|
18
|
+
const types = schema.allOf.map((s) => jsonSchemaToTs(s, indent));
|
|
19
|
+
return types.join(" & ");
|
|
20
|
+
}
|
|
21
|
+
if ("const" in schema) {
|
|
22
|
+
return JSON.stringify(schema.const);
|
|
23
|
+
}
|
|
24
|
+
if ("enum" in schema && Array.isArray(schema.enum)) {
|
|
25
|
+
return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
|
|
26
|
+
}
|
|
27
|
+
const type = schema.type;
|
|
28
|
+
if (type === "string") {
|
|
29
|
+
return "string";
|
|
30
|
+
}
|
|
31
|
+
if (type === "number" || type === "integer") {
|
|
32
|
+
return "number";
|
|
33
|
+
}
|
|
34
|
+
if (type === "boolean") {
|
|
35
|
+
return "boolean";
|
|
36
|
+
}
|
|
37
|
+
if (type === "null") {
|
|
38
|
+
return "null";
|
|
39
|
+
}
|
|
40
|
+
if (type === "array") {
|
|
41
|
+
const items = schema.items;
|
|
42
|
+
if (items) {
|
|
43
|
+
return `${jsonSchemaToTs(items, indent)}[]`;
|
|
44
|
+
}
|
|
45
|
+
return "unknown[]";
|
|
46
|
+
}
|
|
47
|
+
if (type === "object" || "properties" in schema) {
|
|
48
|
+
const properties = schema.properties;
|
|
49
|
+
const required = schema.required || [];
|
|
50
|
+
const additionalProperties = schema.additionalProperties;
|
|
51
|
+
if (!properties || Object.keys(properties).length === 0) {
|
|
52
|
+
if (additionalProperties === false) {
|
|
53
|
+
return "Record<string, never>";
|
|
54
|
+
}
|
|
55
|
+
if (typeof additionalProperties === "object") {
|
|
56
|
+
return `Record<string, ${jsonSchemaToTs(additionalProperties, indent)}>`;
|
|
57
|
+
}
|
|
58
|
+
return "Record<string, unknown>";
|
|
59
|
+
}
|
|
60
|
+
const lines = ["{"];
|
|
61
|
+
const propIndent = " ".repeat(indent + 1);
|
|
62
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
63
|
+
const isRequired = required.includes(key);
|
|
64
|
+
const optional = isRequired ? "" : "?";
|
|
65
|
+
const safeKey = /^[\w$]+$/.test(key) && !/^\d/.test(key) ? key : JSON.stringify(key);
|
|
66
|
+
const propType = jsonSchemaToTs(propSchema, indent + 1);
|
|
67
|
+
lines.push(`${propIndent}${safeKey}${optional}: ${propType}`);
|
|
68
|
+
}
|
|
69
|
+
lines.push(`${spaces}}`);
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(type)) {
|
|
73
|
+
const types = type.map((t) => {
|
|
74
|
+
switch (t) {
|
|
75
|
+
case "null":
|
|
76
|
+
return "null";
|
|
77
|
+
case "string":
|
|
78
|
+
return "string";
|
|
79
|
+
case "number":
|
|
80
|
+
case "integer":
|
|
81
|
+
return "number";
|
|
82
|
+
case "boolean":
|
|
83
|
+
return "boolean";
|
|
84
|
+
case "array":
|
|
85
|
+
return "unknown[]";
|
|
86
|
+
case "object":
|
|
87
|
+
return "Record<string, unknown>";
|
|
88
|
+
default:
|
|
89
|
+
return "unknown";
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return types.join(" | ");
|
|
93
|
+
}
|
|
94
|
+
return "unknown";
|
|
95
|
+
}
|
|
96
|
+
function generateTypeDeclaration(schema) {
|
|
97
|
+
const tsType = jsonSchemaToTs(schema);
|
|
98
|
+
return `// Auto-generated by nuxt-safe-runtime-config
|
|
99
|
+
// Do not edit manually
|
|
100
|
+
|
|
101
|
+
export type SafeRuntimeConfig = ${tsType}
|
|
102
|
+
|
|
103
|
+
declare module '#imports' {
|
|
104
|
+
export function useSafeRuntimeConfig(): SafeRuntimeConfig
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
declare module '#app' {
|
|
108
|
+
export function useSafeRuntimeConfig(): SafeRuntimeConfig
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export {}
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const logger = useLogger("safe-runtime-config");
|
|
3
116
|
const module = defineNuxtModule({
|
|
4
117
|
meta: {
|
|
5
118
|
name: "nuxt-safe-runtime-config",
|
|
6
|
-
configKey: "safeRuntimeConfig"
|
|
119
|
+
configKey: "safeRuntimeConfig",
|
|
120
|
+
compatibility: { nuxt: ">=3.0.0" }
|
|
7
121
|
},
|
|
8
122
|
defaults: {
|
|
9
|
-
$schema: void 0
|
|
123
|
+
$schema: void 0,
|
|
124
|
+
validateAtBuild: true,
|
|
125
|
+
validateAtRuntime: false,
|
|
126
|
+
jsonSchemaTarget: "draft-2020-12",
|
|
127
|
+
onBuildError: "throw",
|
|
128
|
+
onRuntimeError: "throw",
|
|
129
|
+
logSuccess: true,
|
|
130
|
+
logFallback: true
|
|
10
131
|
},
|
|
11
|
-
setup(options, nuxt) {
|
|
132
|
+
async setup(options, nuxt) {
|
|
12
133
|
if (!options.$schema)
|
|
13
134
|
return;
|
|
14
135
|
const schema = options.$schema;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
136
|
+
const resolver = createResolver(import.meta.url);
|
|
137
|
+
const jsonSchema = await getJSONSchema(schema, options.jsonSchemaTarget, options.logFallback);
|
|
138
|
+
addTypeTemplate({
|
|
139
|
+
filename: "types/safe-runtime-config.d.ts",
|
|
140
|
+
getContents: () => generateTypeDeclaration(jsonSchema)
|
|
19
141
|
});
|
|
20
|
-
|
|
21
|
-
|
|
142
|
+
if (options.validateAtBuild) {
|
|
143
|
+
const validateOpts = { onError: options.onBuildError, logSuccess: options.logSuccess };
|
|
144
|
+
nuxt.hook("ready", async () => {
|
|
145
|
+
if (nuxt.options._prepare)
|
|
146
|
+
return;
|
|
147
|
+
await validateRuntimeConfig(nuxt.options.nitro.runtimeConfig, schema, validateOpts);
|
|
148
|
+
});
|
|
149
|
+
nuxt.hook("build:done", async () => {
|
|
150
|
+
await validateRuntimeConfig(nuxt.options.nitro.runtimeConfig, schema, validateOpts);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (options.validateAtRuntime) {
|
|
154
|
+
addTemplate({
|
|
155
|
+
filename: "safe-runtime-config/schema.json",
|
|
156
|
+
write: true,
|
|
157
|
+
getContents: () => JSON.stringify(jsonSchema)
|
|
158
|
+
});
|
|
159
|
+
const schemaUri = jsonSchema.$schema;
|
|
160
|
+
const draft = schemaUri?.includes("2020-12") ? "2020-12" : schemaUri?.includes("2019-09") ? "2019-09" : schemaUri?.includes("draft-07") ? "7" : "2020-12";
|
|
161
|
+
const pluginPath = addTemplate({
|
|
162
|
+
filename: "safe-runtime-config/validate-plugin.ts",
|
|
163
|
+
write: true,
|
|
164
|
+
getContents: () => `
|
|
165
|
+
import { Validator } from '@cfworker/json-schema'
|
|
166
|
+
import { useRuntimeConfig } from '#imports'
|
|
167
|
+
import { consola } from 'consola'
|
|
168
|
+
|
|
169
|
+
const logger = consola.withTag('safe-runtime-config')
|
|
170
|
+
const schema = ${JSON.stringify(jsonSchema)}
|
|
171
|
+
const options = ${JSON.stringify({ onError: options.onRuntimeError, logSuccess: options.logSuccess })}
|
|
172
|
+
|
|
173
|
+
export default defineNitroPlugin(() => {
|
|
174
|
+
const config = useRuntimeConfig()
|
|
175
|
+
const validator = new Validator(schema, '${draft}')
|
|
176
|
+
const result = validator.validate(config)
|
|
177
|
+
|
|
178
|
+
if (!result.valid) {
|
|
179
|
+
const errors = result.errors.map(e => \`\${e.instanceLocation}: \${e.error}\`).join(', ')
|
|
180
|
+
const msg = \`Runtime validation failed: \${errors}\`
|
|
181
|
+
|
|
182
|
+
if (options.onError === 'throw') {
|
|
183
|
+
logger.error(msg)
|
|
184
|
+
throw new Error(msg)
|
|
185
|
+
} else if (options.onError === 'warn') {
|
|
186
|
+
logger.warn(msg)
|
|
187
|
+
}
|
|
188
|
+
} else if (options.logSuccess) {
|
|
189
|
+
logger.success('Runtime config validated (server start)')
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
`
|
|
193
|
+
});
|
|
194
|
+
nuxt.hook("nitro:config", (nitroConfig) => {
|
|
195
|
+
nitroConfig.plugins = nitroConfig.plugins || [];
|
|
196
|
+
nitroConfig.plugins.push(pluginPath.dst);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
addImportsDir(resolver.resolve("./runtime/composables"));
|
|
200
|
+
nuxt.hook("eslint:config:addons", (addons) => {
|
|
201
|
+
addons.push({
|
|
202
|
+
name: "nuxt-safe-runtime-config",
|
|
203
|
+
getConfigs: () => ({
|
|
204
|
+
imports: [{ from: "nuxt-safe-runtime-config/eslint", name: "default", as: "safeRuntimeConfig" }],
|
|
205
|
+
configs: [`safeRuntimeConfig.configs.recommended`]
|
|
206
|
+
})
|
|
207
|
+
});
|
|
22
208
|
});
|
|
23
209
|
}
|
|
24
210
|
});
|
|
25
|
-
|
|
211
|
+
function hasNativeJSONSchema(schema) {
|
|
212
|
+
return typeof schema?.["~standard"]?.jsonSchema?.output === "function";
|
|
213
|
+
}
|
|
214
|
+
async function getJSONSchema(schema, target = "draft-2020-12", logFallback = true) {
|
|
215
|
+
if (hasNativeJSONSchema(schema)) {
|
|
216
|
+
try {
|
|
217
|
+
return schema["~standard"].jsonSchema.output({ target });
|
|
218
|
+
} catch {
|
|
219
|
+
if (logFallback)
|
|
220
|
+
logger.warn(`Native JSON Schema failed for target "${target}", using fallback`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (logFallback)
|
|
224
|
+
logger.warn("Schema does not support native JSON Schema, using @standard-community/standard-json fallback");
|
|
225
|
+
return await toJsonSchema(schema);
|
|
226
|
+
}
|
|
227
|
+
async function validateRuntimeConfig(config, schema, opts) {
|
|
26
228
|
if (!isStandardSchema(schema)) {
|
|
27
|
-
|
|
28
|
-
|
|
229
|
+
const msg = "Schema is not Standard Schema compatible";
|
|
230
|
+
if (opts.onError === "throw") {
|
|
231
|
+
logger.error(msg);
|
|
232
|
+
throw new Error("Invalid schema format");
|
|
233
|
+
} else if (opts.onError === "warn") {
|
|
234
|
+
logger.warn(msg);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
29
237
|
}
|
|
30
238
|
const result = await schema["~standard"].validate(config);
|
|
31
239
|
if ("issues" in result && result.issues && result.issues.length > 0) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
240
|
+
const errorLines = result.issues.map((issue, index) => ` ${index + 1}. ${formatIssue(issue)}`);
|
|
241
|
+
if (opts.onError === "throw") {
|
|
242
|
+
logger.error(`Validation failed!
|
|
243
|
+
${errorLines.join("\n")}`);
|
|
244
|
+
throw new Error("Runtime config validation failed");
|
|
245
|
+
} else if (opts.onError === "warn") {
|
|
246
|
+
logger.warn(`Validation failed!
|
|
247
|
+
${errorLines.join("\n")}`);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
37
250
|
}
|
|
38
|
-
|
|
251
|
+
if (opts.logSuccess)
|
|
252
|
+
logger.success("Validated Runtime Config");
|
|
39
253
|
}
|
|
40
254
|
function isStandardSchema(schema) {
|
|
41
255
|
return schema && typeof schema === "object" && "~standard" in schema && typeof schema["~standard"] === "object" && typeof schema["~standard"].validate === "function";
|
package/dist/types.d.mts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import type { ModuleHooks, ModuleRuntimeHooks, ModuleRuntimeConfig, ModulePublicRuntimeConfig } from './module.mjs'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
declare module '#app' {
|
|
4
|
+
interface RuntimeNuxtHooks extends ModuleRuntimeHooks {}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
declare module '@nuxt/schema' {
|
|
8
|
+
interface NuxtHooks extends ModuleHooks {}
|
|
9
|
+
interface RuntimeConfig extends ModuleRuntimeConfig {}
|
|
10
|
+
interface PublicRuntimeConfig extends ModulePublicRuntimeConfig {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export * from "./module.mjs"
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-safe-runtime-config",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.13",
|
|
5
|
+
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0",
|
|
5
6
|
"description": "Validate Nuxt runtime config with Standard Schema at build time",
|
|
6
7
|
"author": {
|
|
7
8
|
"name": "onmax"
|
|
@@ -20,64 +21,75 @@
|
|
|
20
21
|
"build-time"
|
|
21
22
|
],
|
|
22
23
|
"exports": {
|
|
23
|
-
".": {
|
|
24
|
-
|
|
25
|
-
"import": "./dist/module.mjs"
|
|
26
|
-
}
|
|
24
|
+
".": { "types": "./dist/types.d.mts", "import": "./dist/module.mjs" },
|
|
25
|
+
"./eslint": { "types": "./dist/eslint.d.mts", "import": "./dist/eslint.mjs" }
|
|
27
26
|
},
|
|
28
27
|
"main": "./dist/module.mjs",
|
|
29
28
|
"typesVersions": {
|
|
30
29
|
"*": {
|
|
31
|
-
".": [
|
|
32
|
-
|
|
33
|
-
]
|
|
30
|
+
".": ["./dist/types.d.mts"],
|
|
31
|
+
"eslint": ["./dist/eslint.d.mts"]
|
|
34
32
|
}
|
|
35
33
|
},
|
|
36
34
|
"files": [
|
|
37
35
|
"dist"
|
|
38
36
|
],
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"@nuxt/kit": "^3.17.4",
|
|
41
|
-
"@standard-schema/spec": "^1.0.0"
|
|
42
|
-
},
|
|
43
|
-
"devDependencies": {
|
|
44
|
-
"@antfu/eslint-config": "^4.13.3",
|
|
45
|
-
"@antfu/ni": "^25.0.0",
|
|
46
|
-
"@nuxt/devtools": "^2.4.1",
|
|
47
|
-
"@nuxt/eslint": "^1.4.1",
|
|
48
|
-
"@nuxt/eslint-config": "^1.4.1",
|
|
49
|
-
"@nuxt/module-builder": "^1.0.1",
|
|
50
|
-
"@nuxt/schema": "^3.17.4",
|
|
51
|
-
"@nuxt/test-utils": "^3.19.1",
|
|
52
|
-
"@types/node": "latest",
|
|
53
|
-
"bumpp": "^10.1.1",
|
|
54
|
-
"changelogen": "^0.6.1",
|
|
55
|
-
"eslint": "^9.28.0",
|
|
56
|
-
"eslint-plugin-format": "^1.0.1",
|
|
57
|
-
"lint-staged": "^16.1.0",
|
|
58
|
-
"nuxt": "^3.17.4",
|
|
59
|
-
"simple-git-hooks": "^2.13.0",
|
|
60
|
-
"typescript": "~5.8.3",
|
|
61
|
-
"valibot": "^1.1.0",
|
|
62
|
-
"vitest": "^3.2.0",
|
|
63
|
-
"vue-tsc": "^2.2.10"
|
|
64
|
-
},
|
|
65
|
-
"simple-git-hooks": {
|
|
66
|
-
"pre-commit": "pnpm lint-staged"
|
|
67
|
-
},
|
|
68
|
-
"lint-staged": {
|
|
69
|
-
"*": "eslint --fix"
|
|
70
|
-
},
|
|
71
37
|
"scripts": {
|
|
38
|
+
"prepack": "nuxt-module-build build && unbuild",
|
|
72
39
|
"dev": "nuxi dev playground",
|
|
73
40
|
"dev:build": "nuxi build playground",
|
|
74
|
-
"dev:prepare": "nuxt-module-build build --stub &&
|
|
75
|
-
"release": "
|
|
41
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxi prepare playground",
|
|
42
|
+
"release": "bumpp && git push --follow-tags",
|
|
76
43
|
"typecheck": "nuxt typecheck && pnpm -C playground typecheck",
|
|
77
44
|
"lint": "eslint .",
|
|
78
45
|
"lint:fix": "eslint . --fix",
|
|
79
46
|
"test": "vitest run",
|
|
80
47
|
"test:watch": "vitest watch",
|
|
81
48
|
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"eslint": "^9.0.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"eslint": { "optional": true }
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@cfworker/json-schema": "catalog:",
|
|
58
|
+
"@nuxt/kit": "catalog:",
|
|
59
|
+
"@standard-community/standard-json": "catalog:",
|
|
60
|
+
"@standard-schema/spec": "catalog:",
|
|
61
|
+
"consola": "catalog:"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@antfu/eslint-config": "catalog:",
|
|
65
|
+
"@antfu/ni": "catalog:",
|
|
66
|
+
"@nuxt/devtools": "catalog:",
|
|
67
|
+
"@nuxt/eslint": "catalog:",
|
|
68
|
+
"@nuxt/eslint-config": "catalog:",
|
|
69
|
+
"@nuxt/module-builder": "catalog:",
|
|
70
|
+
"@nuxt/schema": "catalog:",
|
|
71
|
+
"@nuxt/test-utils": "catalog:",
|
|
72
|
+
"@types/node": "catalog:",
|
|
73
|
+
"@typescript-eslint/parser": "catalog:",
|
|
74
|
+
"@typescript-eslint/rule-tester": "catalog:",
|
|
75
|
+
"@typescript-eslint/utils": "catalog:",
|
|
76
|
+
"bumpp": "catalog:",
|
|
77
|
+
"changelogen": "catalog:",
|
|
78
|
+
"eslint": "catalog:",
|
|
79
|
+
"eslint-plugin-format": "catalog:",
|
|
80
|
+
"lint-staged": "catalog:",
|
|
81
|
+
"nuxt": "catalog:",
|
|
82
|
+
"simple-git-hooks": "catalog:",
|
|
83
|
+
"typescript": "catalog:",
|
|
84
|
+
"unbuild": "catalog:",
|
|
85
|
+
"valibot": "catalog:",
|
|
86
|
+
"vitest": "catalog:",
|
|
87
|
+
"vue-tsc": "catalog:"
|
|
88
|
+
},
|
|
89
|
+
"simple-git-hooks": {
|
|
90
|
+
"pre-commit": "pnpm lint-staged"
|
|
91
|
+
},
|
|
92
|
+
"lint-staged": {
|
|
93
|
+
"*": "eslint --fix"
|
|
82
94
|
}
|
|
83
|
-
}
|
|
95
|
+
}
|