@cooperco/nuxt-layer-base 1.0.0 → 1.0.2

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 ADDED
@@ -0,0 +1,287 @@
1
+ # Base Layer
2
+
3
+ A foundational layer that provides essential configuration and tooling for Nuxt 4 projects.
4
+
5
+ ## Features
6
+
7
+ - TypeScript integration with strict mode enabled
8
+ - ESLint configuration with stylistic rules for Vue templates
9
+ - Test utilities integration
10
+ - Internationalization (i18n) via `@nuxtjs/i18n`
11
+ - Nuxt DevTools enabled
12
+ - CI/CD workflows for code quality checks
13
+
14
+ ## Development
15
+
16
+ ```bash
17
+ # Install dependencies
18
+ npm install
19
+
20
+ # Start development server
21
+ npm run dev
22
+
23
+ # Run ESLint
24
+ npm run lint
25
+
26
+ # Fix linting issues automatically
27
+ npm run lint:fix
28
+
29
+ # Run TypeScript type checking
30
+ npm run typecheck
31
+ ```
32
+
33
+ ## Configuration Details
34
+
35
+ ### Key Features
36
+
37
+ - **Compatibility Date:** 2025-07-15
38
+ - **Strict TypeScript:** Enforces type safety throughout your projects
39
+ - **ESLint Integration:** Pre-configured with stylistic rules for Vue templates
40
+ - Custom ESLint rules for code style consistency
41
+ - Vue template formatting rules
42
+ - **Testing Utilities:** Built-in test utils for comprehensive testing
43
+
44
+ ### Configuration Details
45
+
46
+ The base layer includes:
47
+
48
+ - Nuxt ESLint module (`@nuxt/eslint`)
49
+ - Nuxt Test Utils module (`@nuxt/test-utils`)
50
+ - Nuxt i18n module (`@nuxtjs/i18n`)
51
+ - Nuxt DevTools enabled
52
+ - TypeScript configuration with strict mode
53
+ - Custom ESLint configuration with rules for:
54
+ - JavaScript stylistic preferences
55
+ - TypeScript unused variables handling
56
+ - Vue template formatting
57
+
58
+ ## Internationalization (i18n)
59
+
60
+ This layer integrates the Nuxt i18n module (`@nuxtjs/i18n`). You can override or extend its configuration in your application.
61
+
62
+ - Docs: https://i18n.nuxtjs.org
63
+
64
+ Override example in a consuming app:
65
+
66
+ ```ts
67
+ // nuxt.config.ts (in your app)
68
+ export default defineNuxtConfig({
69
+ extends: ['@cooperco/nuxt-layer-base'],
70
+ i18n: {
71
+ locales: [
72
+ { code: 'en', language: 'en-US', name: 'English' },
73
+ { code: 'fr', language: 'fr-FR', name: 'Français' }
74
+ ],
75
+ defaultLocale: 'en'
76
+ }
77
+ })
78
+ ```
79
+
80
+ Basic usage in a component:
81
+
82
+ ```vue
83
+ <script setup lang="ts">
84
+ const { t, locale } = useI18n()
85
+ </script>
86
+
87
+ <template>
88
+ <p>{{ t('hello') }}</p>
89
+ </template>
90
+ ```
91
+
92
+ Note: Provide your own translation files (for example, `locales/en.json`) in your application.
93
+
94
+ ## Usage
95
+
96
+ To use this layer in your Nuxt project:
97
+
98
+ ```typescript
99
+ // nuxt.config.ts
100
+ export default defineNuxtConfig({
101
+ extends: [
102
+ '@cooperco/nuxt-layer-base'
103
+ ]
104
+ })
105
+ ```
106
+
107
+ ## Nuxt 4 directory structure
108
+ This layer follows Nuxt 4's app directory structure:
109
+ - app/plugins/... for plugins (e.g., app/plugins/loggly.client.ts)
110
+ - app/composables/... for composables (e.g., app/composables/useLoggly.ts)
111
+
112
+ If you previously looked under plugins/ or composables/ at the layer root, note they have been relocated under app/ for Nuxt 4.
113
+
114
+ ## Error logging (Loggly)
115
+
116
+ This base layer includes optional, secure error logging via Loggly using a server proxy. It is off by default and can be enabled per consuming app using environment variables.
117
+
118
+ What you get when enabled:
119
+ - Automatic server-side error capture (Nitro request errors)
120
+ - Automatic client-side error capture (Nuxt/Vue/global browser errors)
121
+ - Composable for custom logs from your features (useLoggly)
122
+ - Centralized PII scrubbing on the server proxy before forwarding to Loggly
123
+
124
+ ### 1) Enable in your app (env vars)
125
+ Set the following environment variables in your consuming app (do not expose the token on the client):
126
+
127
+ - LOGGLY_ENABLED=true
128
+ - LOGGLY_TOKEN=your-loggly-customer-token
129
+ - LOGGLY_TAGS=nuxt,base,app-name,env (comma-separated; optional)
130
+ - LOG_LEVEL=error (optional; error|warn|info|debug)
131
+ - LOGGLY_ENDPOINT=https://logs-01.loggly.com/inputs (optional override)
132
+
133
+ Notes
134
+ - LOGGLY_TOKEN is server-only and never shipped to the client. The client sends logs to the server proxy at /api/loggly.
135
+ - If LOGGLY_ENABLED is not true or the token is missing, logging is skipped.
136
+
137
+ ### 2) Using the composable in your app
138
+ The base layer exposes a composable you can call anywhere in your app.
139
+
140
+ Example:
141
+ ```ts
142
+ // inside a component or any composable
143
+ const log = useLoggly()
144
+ await log.error('Checkout failed', { orderId, step: 'place-order' }, ['checkout'])
145
+ await log.info('User clicked CTA', { campaign: 'summer' }, ['marketing'])
146
+ ```
147
+
148
+ API:
149
+ - error(message, meta?, tags?)
150
+ - warn(message, meta?, tags?)
151
+ - info(message, meta?, tags?)
152
+ - debug(message, meta?, tags?)
153
+
154
+ Tags are merged with your global LOGGLY_TAGS (duplicates removed, limited to 10).
155
+
156
+ ### 3) Auto-captured errors
157
+ When enabled, the base layer automatically forwards:
158
+ - Server: request-time exceptions via a Nitro plugin
159
+ - Client: Nuxt app errors, Vue component errors, window error events, and unhandled promise rejections
160
+
161
+ These are forwarded to /api/loggly, which safely shapes the payload before sending to Loggly.
162
+
163
+ ### 4) Security and privacy
164
+ - The server proxy masks common sensitive keys (password, token, authorization, etc.) in meta payloads.
165
+ - Prefer adding any app-specific sensitive keys to your meta scrubbing in the proxy if needed.
166
+ - Logging is fire-and-forget; failures to log do not block requests or UI.
167
+
168
+ ### 5) Verifying locally
169
+ 1) In your consuming app, set env vars (e.g. in .env.local):
170
+ ```
171
+ LOGGLY_ENABLED=true
172
+ LOGGLY_TOKEN=...your token...
173
+ LOGGLY_TAGS=nuxt,base,local,app-name
174
+ LOG_LEVEL=error
175
+ ```
176
+ 2) Start your app and induce an error (e.g., throw new Error('Test') in onMounted of a test component).
177
+ 3) Watch your network tab for POST /api/loggly (200 OK). Then check Loggly for the entry with your tags.
178
+
179
+ ### 6) Troubleshooting
180
+ - No logs? Ensure LOGGLY_ENABLED=true and LOGGLY_TOKEN is set in the server environment.
181
+ - CORS or network errors from the browser? The client never talks to Loggly directly; it only posts to /api/loggly.
182
+ - Too much volume? Consider adding sampling or batching later; the proxy makes this easy to change centrally.
183
+
184
+
185
+ ## Playground: Test Loggly in the base layer
186
+
187
+ A tiny playground app is included so you can run the base layer by itself and verify Loggly end‑to‑end before integrating into another app.
188
+
189
+ Location: playground
190
+
191
+ How it works:
192
+ - The playground extends the base layer (extends: ['../layers/base']).
193
+ - It includes a simple page to send logs via useLoggly and buttons to trigger client/server errors.
194
+ - The server exposes GET /api/boom to intentionally throw and test server‑side logging.
195
+
196
+ Run it:
197
+ 1) From the repo root:
198
+ - cd playground && npm run dev
199
+
200
+ 2) Provide environment variables for Loggly in the playground (optional but required to actually send to Loggly):
201
+ Copy playground/.env.example to playground/.env and fill in your values.
202
+
203
+ LOGGLY_ENABLED=true
204
+ LOGGLY_TOKEN=your-loggly-customer-token
205
+ LOGGLY_TAGS=nuxt,base,playground,local
206
+ LOG_LEVEL=debug
207
+ # Optional override
208
+ # LOGGLY_ENDPOINT=https://logs-01.loggly.com/inputs
209
+
210
+ 3) Open http://localhost:3000
211
+ - Click the Send error/warn/info/debug buttons to send custom logs.
212
+ - Click Throw client error to exercise the client plugin.
213
+ - Click Call /api/boom to exercise the server plugin.
214
+
215
+ Notes:
216
+ - If LOGGLY_ENABLED is not true or LOGGLY_TOKEN is missing, the server proxy will skip sending and respond with { ok: false, skipped: true }.
217
+ - The token is server‑only; it is never exposed to the client. The browser only talks to /api/loggly on your dev server.
218
+
219
+
220
+ ---
221
+
222
+ ## Publishing to npm (tag-based)
223
+
224
+ This layer is published using GitHub Actions when you push a tag that matches the base-vX.Y.Z pattern.
225
+
226
+ High-level flow:
227
+ - Bump the version in layers/base/package.json (SemVer).
228
+ - Commit and push your changes to main (or ensure the commit is on main).
229
+ - Create and push a tag named base-vX.Y.Z (matching the package.json version).
230
+ - The Publish Base Layer workflow installs deps, checks if that exact version already exists on npm, and if not, publishes privately with --access restricted.
231
+
232
+ Important notes:
233
+ - Do NOT rely on npm version creating a tag for you (it will create vX.Y.Z). We use a custom tag prefix base-v.
234
+ - First-time private publish is already configured via publishConfig.access: "restricted".
235
+ - The workflow will skip if the version already exists (you'll see "Skipping; version already exists.").
236
+
237
+ ### Step-by-step
238
+
239
+ 1) Bump the version in layers/base/package.json
240
+ - Option A (recommended): use npm version without creating a tag
241
+ - Bash:
242
+ ```bash
243
+ cd layers/base
244
+ npm version patch --no-git-tag-version # or minor | major
245
+ ```
246
+ - PowerShell:
247
+ ```powershell
248
+ Set-Location layers/base
249
+ npm version patch --no-git-tag-version # or minor | major
250
+ ```
251
+ - Option B: manually edit the version field in layers/base/package.json (SemVer: MAJOR.MINOR.PATCH)
252
+
253
+ 2) Commit and push the change (from repo root or layers/base)
254
+ ```bash
255
+ git add layers/base/package.json
256
+ git commit -m "chore(base): bump version"
257
+ git push origin main
258
+ ```
259
+
260
+ 3) Create and push the tag using the new version
261
+ - Get the new version value:
262
+ - Bash:
263
+ ```bash
264
+ cd layers/base
265
+ VERSION=$(node -p "require('./package.json').version")
266
+ cd ../..
267
+ git tag "base-v$VERSION"
268
+ git push origin "base-v$VERSION"
269
+ ```
270
+ - PowerShell:
271
+ ```powershell
272
+ Set-Location layers/base
273
+ $version = node -p "require('./package.json').version"
274
+ Set-Location ../..
275
+ git tag "base-v$version"
276
+ git push origin "base-v$version"
277
+ ```
278
+
279
+ 4) GitHub Actions will publish
280
+ - Workflow: .github/workflows/publish-base.yml
281
+ - Auth: uses NPM_TOKEN (Classic Automation token) configured as a GitHub secret
282
+ - Behavior: installs, checks npm view @cooperco/nuxt-layer-base@<version>, publishes if not found
283
+
284
+ ### Troubleshooting
285
+ - Version already exists: bump the version again (patch/minor/major) and push a new tag.
286
+ - Auth errors (401/403): ensure NPM_TOKEN is set in repo secrets and org access is correct.
287
+ - Registry mismatch: this repo’s .npmrc pins @cooperco to https://registry.npmjs.org/; keep that file intact.
@@ -0,0 +1,28 @@
1
+ export type LogLevel = 'error' | 'warn' | 'info' | 'debug'
2
+
3
+ export const useLoggly = () => {
4
+ const { public: pub } = useRuntimeConfig()
5
+ const enabled = !!pub?.logglyEnabled
6
+ const baseTags = (pub?.logglyTags || []) as string[]
7
+
8
+ const send = (
9
+ level: LogLevel,
10
+ message: string,
11
+ meta?: Record<string, unknown>,
12
+ tags: string[] = []
13
+ ) => {
14
+ if (!enabled) return
15
+ const mergedTags = [...new Set([...baseTags, ...tags])].slice(0, 10)
16
+ return $fetch('/api/loggly', {
17
+ method: 'POST',
18
+ body: { level, message, meta, tags: mergedTags }
19
+ }).catch(() => {})
20
+ }
21
+
22
+ return {
23
+ error: (m: string, meta?: Record<string, unknown>, tags?: string[]) => send('error', m, meta, tags),
24
+ warn: (m: string, meta?: Record<string, unknown>, tags?: string[]) => send('warn', m, meta, tags),
25
+ info: (m: string, meta?: Record<string, unknown>, tags?: string[]) => send('info', m, meta, tags),
26
+ debug: (m: string, meta?: Record<string, unknown>, tags?: string[]) => send('debug', m, meta, tags)
27
+ }
28
+ }
@@ -0,0 +1,70 @@
1
+ export default defineNuxtPlugin((nuxtApp) => {
2
+ const config = useRuntimeConfig()
3
+ if (!config.public?.logglyEnabled) return
4
+
5
+ const post = (payload: Record<string, unknown>) =>
6
+ $fetch('/api/loggly', { method: 'POST', body: payload }).catch(() => { /* swallow */ })
7
+
8
+ // Nuxt app-level errors
9
+ nuxtApp.hook('app:error', (err) => {
10
+ post({
11
+ level: 'error',
12
+ message: extractMessage(err) || 'App error',
13
+ error: toErrorShape(err),
14
+ tags: ['client', 'nuxt']
15
+ })
16
+ })
17
+
18
+ // Vue component-level errors
19
+ const originalHandler = nuxtApp.vueApp.config.errorHandler
20
+ nuxtApp.vueApp.config.errorHandler = (err, instance, info) => {
21
+ post({
22
+ level: 'error',
23
+ message: extractMessage(err) || 'Vue error',
24
+ error: toErrorShape(err),
25
+ meta: { info, component: getComponentName(instance) },
26
+ tags: ['client', 'vue']
27
+ })
28
+ if (originalHandler) originalHandler(err, instance, info)
29
+ }
30
+
31
+ if (import.meta.client) {
32
+ window.addEventListener('error', (event: ErrorEvent) => {
33
+ post({ level: 'error', message: event.message, error: toErrorShape(event.error), tags: ['client', 'window'] })
34
+ })
35
+ window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
36
+ const reason = event.reason
37
+ post({ level: 'error', message: extractMessage(reason) || 'Unhandled rejection', error: toErrorShape(reason), tags: ['client', 'promise'] })
38
+ })
39
+ }
40
+ })
41
+
42
+ const toErrorShape = (err: unknown) => {
43
+ if (!err || typeof err !== 'object') return undefined
44
+ const e = err as { name?: string, message?: unknown, stack?: unknown }
45
+ return {
46
+ name: e.name || 'Error',
47
+ message: String(e.message ?? 'Error'),
48
+ stack: typeof e.stack === 'string' ? e.stack.split('\n').slice(0, 20) : undefined
49
+ }
50
+ }
51
+
52
+ const extractMessage = (err: unknown): string | undefined => {
53
+ if (!err) return undefined
54
+ if (typeof err === 'string') return err
55
+ if (typeof err === 'object' && 'message' in err) {
56
+ return String((err as { message?: unknown }).message)
57
+ }
58
+ return undefined
59
+ }
60
+
61
+ const getComponentName = (instance: unknown): string | undefined => {
62
+ if (!instance) return undefined
63
+ const rec = instance as Record<string, unknown>
64
+ const typeVal = rec?.type as unknown
65
+ if (typeof typeVal === 'string') return typeVal
66
+ if (typeVal && typeof typeVal === 'object' && 'name' in typeVal) {
67
+ return String((typeVal as { name?: unknown }).name)
68
+ }
69
+ return undefined
70
+ }
package/nuxt.config.ts CHANGED
@@ -1,8 +1,23 @@
1
+ /* eslint-disable nuxt/nuxt-config-keys-order */
1
2
  export default defineNuxtConfig({
2
- modules: ['@nuxt/eslint', '@nuxt/test-utils'],
3
+ modules: ['@nuxt/eslint', '@nuxtjs/i18n'],
3
4
  devtools: { enabled: true },
4
5
  compatibilityDate: '2025-07-15',
5
6
 
7
+ runtimeConfig: {
8
+ // server-only
9
+ logglyToken: process.env.LOGGLY_TOKEN || '',
10
+ logglyEndpoint: process.env.LOGGLY_ENDPOINT || 'https://logs-01.loggly.com/inputs',
11
+
12
+ // public (safe to expose)
13
+ public: {
14
+ // Allow either LOGGLY_ENABLED or NUXT_PUBLIC_LOGGLY_ENABLED to enable client features
15
+ logglyEnabled: (process.env.NUXT_PUBLIC_LOGGLY_ENABLED ?? process.env.LOGGLY_ENABLED) === 'true',
16
+ logglyTags: (process.env.NUXT_PUBLIC_LOGGLY_TAGS ?? process.env.LOGGLY_TAGS ?? 'nuxt,base').split(',').map(s => s.trim()).filter(Boolean),
17
+ logLevel: (process.env.NUXT_PUBLIC_LOG_LEVEL ?? process.env.LOG_LEVEL) || 'error' // error|warn|info|debug
18
+ }
19
+ },
20
+
6
21
  typescript: {
7
22
  tsConfig: {
8
23
  compilerOptions: {
@@ -13,7 +28,14 @@ export default defineNuxtConfig({
13
28
 
14
29
  eslint: {
15
30
  config: {
16
- stylistic: true // needed for Vue Template linting rules
31
+ stylistic: true
17
32
  }
33
+ },
34
+
35
+ i18n: {
36
+ locales: [
37
+ { code: 'enUS', language: 'en-US' }
38
+ ],
39
+ defaultLocale: 'enUS'
18
40
  }
19
41
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cooperco/nuxt-layer-base",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "scripts": {
@@ -11,7 +11,8 @@
11
11
  },
12
12
  "description": "Base Nuxt layer for cooperco projects",
13
13
  "publishConfig": {
14
- "access": "public"
14
+ "access": "public",
15
+ "registry": "https://registry.npmjs.org/"
15
16
  },
16
17
  "repository": {
17
18
  "type": "git",
@@ -21,10 +22,13 @@
21
22
  "author": "cooperco",
22
23
  "license": "MIT",
23
24
  "dependencies": {
24
- "@nuxt/eslint": "^1.7.1",
25
+ "@nuxt/eslint": "^1.9.0",
26
+ "@nuxtjs/i18n": "^10.0.6",
27
+ "nuxt": "^4.0.3"
28
+ },
29
+ "devDependencies": {
25
30
  "@nuxt/test-utils": "^3.19.2",
26
- "eslint": "^9.32.0",
27
- "nuxt": "^4.0.0",
28
- "vue-tsc": "^3.0.4"
31
+ "eslint": "^9.33.0",
32
+ "vue-tsc": "^3.0.5"
29
33
  }
30
34
  }
@@ -0,0 +1,58 @@
1
+ import { defineEventHandler, readBody } from 'h3'
2
+
3
+ type LogglyBody = {
4
+ level?: string
5
+ message?: string
6
+ error?: unknown
7
+ tags?: string[]
8
+ meta?: Record<string, unknown>
9
+ }
10
+
11
+ const sanitizeMeta = (input: unknown) => {
12
+ if (!input || typeof input !== 'object') return undefined
13
+ const blocked = ['password', 'token', 'authorization', 'auth', 'ssn', 'creditcard', 'credit_card']
14
+ const out: Record<string, unknown> = {}
15
+ for (const [k, v] of Object.entries(input as Record<string, unknown>)) {
16
+ out[k] = blocked.includes(k.toLowerCase()) ? '[REDACTED]' : v
17
+ }
18
+ return out
19
+ }
20
+
21
+ const serializeError = (err: unknown) => {
22
+ if (!err || typeof err !== 'object') return undefined
23
+ const e = err as { name?: string, message?: unknown, stack?: unknown }
24
+ return {
25
+ name: e.name || 'Error',
26
+ message: String(e.message ?? 'Error'),
27
+ stack: typeof e.stack === 'string' ? e.stack.split('\n').slice(0, 20) : undefined
28
+ }
29
+ }
30
+
31
+ export default defineEventHandler(async (event) => {
32
+ const { logglyToken, logglyEndpoint, public: pub } = useRuntimeConfig()
33
+ if (!pub?.logglyEnabled || !logglyToken) {
34
+ return { ok: false, skipped: true }
35
+ }
36
+
37
+ const body = await readBody<LogglyBody>(event)
38
+
39
+ const scrubbed = {
40
+ level: body?.level ?? 'error',
41
+ message: body?.message?.toString().slice(0, 5000),
42
+ tags: [...new Set([...(pub.logglyTags || []), ...(body?.tags || [])])].slice(0, 10),
43
+ meta: sanitizeMeta(body?.meta),
44
+ error: body?.error ? serializeError(body.error) : undefined,
45
+ ts: new Date().toISOString()
46
+ }
47
+
48
+ const url = `${logglyEndpoint}/${encodeURIComponent(logglyToken)}/tag/${encodeURIComponent(scrubbed.tags.join(','))}`
49
+
50
+ try {
51
+ await $fetch(url, { method: 'POST', body: scrubbed })
52
+ return { ok: true }
53
+ }
54
+ catch {
55
+ // do not throw here to avoid cascading failures
56
+ return { ok: false }
57
+ }
58
+ })
@@ -0,0 +1,46 @@
1
+ type NitroErrorCtx = {
2
+ event?: {
3
+ path?: string
4
+ method?: string
5
+ node?: { res?: { statusCode?: number } }
6
+ }
7
+ }
8
+
9
+ const toErrorShape = (err: unknown) => {
10
+ if (!err || typeof err !== 'object') {
11
+ return undefined
12
+ }
13
+ const e = err as { name?: string, message?: unknown, stack?: unknown }
14
+ return {
15
+ name: e.name || 'Error',
16
+ message: String(e.message ?? 'Error'),
17
+ stack: typeof e.stack === 'string' ? e.stack.split('\n').slice(0, 20) : undefined
18
+ }
19
+ }
20
+
21
+ export default defineNitroPlugin((nitroApp) => {
22
+ const config = useRuntimeConfig()
23
+ if (!config.public?.logglyEnabled || !config.logglyToken) return
24
+
25
+ nitroApp.hooks.hook('error', async (error: unknown, ctx?: NitroErrorCtx) => {
26
+ try {
27
+ await $fetch('/api/loggly', {
28
+ method: 'POST',
29
+ body: {
30
+ level: 'error',
31
+ message: (typeof error === 'object' && error && 'message' in error) ? String((error as { message?: unknown }).message) : 'Server error',
32
+ error: toErrorShape(error),
33
+ meta: {
34
+ url: ctx?.event?.path,
35
+ method: ctx?.event?.method,
36
+ status: ctx?.event?.node?.res?.statusCode
37
+ },
38
+ tags: ['server']
39
+ }
40
+ })
41
+ }
42
+ catch {
43
+ // swallow logging errors
44
+ }
45
+ })
46
+ })