@stonecrop/nuxt 0.13.8 → 0.13.10

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/dist/module.d.mts CHANGED
@@ -48,7 +48,7 @@ type RouteStrategyFn = (doctypes: ParsedDoctype[]) => NuxtPage[];
48
48
  interface ModuleOptions {
49
49
  /** Enable the DocBuilder feature with /docbuilder routes */
50
50
  docbuilder?: boolean;
51
- /** Path to doctypes folder relative to srcDir (defaults to 'doctypes', resolving to app/doctypes) */
51
+ /** Path to doctypes folder relative to the project root (defaults to 'doctypes') */
52
52
  doctypesDir?: string;
53
53
  /**
54
54
  * Path to the page component used for default slug-based routing.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stonecrop/nuxt",
3
3
  "configKey": "stonecrop",
4
- "version": "0.13.8",
4
+ "version": "0.13.10",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -99,7 +99,7 @@ const module$1 = defineNuxtModule({
99
99
  const homepage = resolve("runtime/app/layouts/StonecropHome.vue");
100
100
  addLayout(homepage, "home");
101
101
  const appDir = nuxt.options.srcDir;
102
- const doctypesDir = resolve(appDir, options.doctypesDir ?? "doctypes");
102
+ const doctypesDir = resolve(nuxt.options.rootDir, options.doctypesDir ?? "doctypes");
103
103
  nuxt.options.runtimeConfig.stonecrop = {
104
104
  ...nuxt.options.runtimeConfig.stonecrop ?? {},
105
105
  doctypesDir
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonecrop/nuxt",
3
- "version": "0.13.8",
3
+ "version": "0.13.10",
4
4
  "description": "Nuxt module for Stonecrop",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,14 +44,14 @@
44
44
  "jiti": "^2.4.2",
45
45
  "pathe": "^2.0.3",
46
46
  "prompts": "^2.4.2",
47
- "@stonecrop/atable": "0.13.8",
48
- "@stonecrop/casl-middleware": "0.13.8",
49
- "@stonecrop/node-editor": "0.13.8",
50
- "@stonecrop/aform": "0.13.8",
51
- "@stonecrop/graphql-middleware": "0.13.8",
52
- "@stonecrop/nuxt-grafserv": "0.13.8",
53
- "@stonecrop/stonecrop": "0.13.8",
54
- "@stonecrop/schema": "0.13.8"
47
+ "@stonecrop/aform": "0.13.10",
48
+ "@stonecrop/casl-middleware": "0.13.10",
49
+ "@stonecrop/atable": "0.13.10",
50
+ "@stonecrop/node-editor": "0.13.10",
51
+ "@stonecrop/graphql-middleware": "0.13.10",
52
+ "@stonecrop/nuxt-grafserv": "0.13.10",
53
+ "@stonecrop/stonecrop": "0.13.10",
54
+ "@stonecrop/schema": "0.13.10"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@eslint/js": "^10.0.1",
package/src/cli/index.ts CHANGED
@@ -144,7 +144,8 @@ export async function runInstaller(options: InstallerOptions): Promise<void> {
144
144
  let stepNum = 2
145
145
 
146
146
  if (selectedFeatures.graphql) {
147
- consola.info(`${stepNum}. Update your GraphQL schema and resolvers:`)
147
+ consola.info(`${stepNum}. Your GraphQL server works out of the box with in-memory sample data.`)
148
+ consola.info(` To connect a real database later, see:`)
148
149
  consola.info(` - server/schema.graphql`)
149
150
  consola.info(` - server/resolvers.ts`)
150
151
  console.log()
@@ -161,8 +162,8 @@ export async function runInstaller(options: InstallerOptions): Promise<void> {
161
162
 
162
163
  if (selectedFeatures.doctypes) {
163
164
  consola.info(`${stepNum}. Customize your doctypes:`)
164
- consola.info(` - doctypes/Example.json`)
165
- consola.info(` - doctypes/example-table.json`)
165
+ consola.info(` - doctypes/Project.json`)
166
+ consola.info(` - doctypes/Task.json`)
166
167
  console.log()
167
168
  }
168
169
 
@@ -4,12 +4,10 @@
4
4
  */
5
5
 
6
6
  import { existsSync } from 'node:fs'
7
- import { mkdir, writeFile, readFile } from 'node:fs/promises'
8
- import { join, dirname } from 'pathe'
9
- import { fileURLToPath } from 'node:url'
7
+ import { mkdir, writeFile } from 'node:fs/promises'
8
+ import { join } from 'pathe'
10
9
  import consola from 'consola'
11
-
12
- const __dirname = dirname(fileURLToPath(import.meta.url))
10
+ import { loadTemplate } from '../utils/templates'
13
11
 
14
12
  export interface DoctypesInstallerOptions {
15
13
  cwd: string
@@ -32,24 +30,24 @@ export async function installDoctypes(options: DoctypesInstallerOptions): Promis
32
30
  consola.info('Created doctypes/ directory')
33
31
  }
34
32
 
35
- // Scaffold Example.json (form doctype)
36
- const examplePath = join(doctypesDir, 'Example.json')
37
- if (!existsSync(examplePath)) {
38
- const exampleTemplate = await loadTemplate('Example.json')
39
- await writeFile(examplePath, exampleTemplate, 'utf-8')
40
- consola.info('Created doctypes/Example.json')
33
+ // Scaffold Project.json
34
+ const projectPath = join(doctypesDir, 'Project.json')
35
+ if (!existsSync(projectPath)) {
36
+ const projectTemplate = await loadTemplate('Project.json')
37
+ await writeFile(projectPath, projectTemplate, 'utf-8')
38
+ consola.info('Created doctypes/Project.json')
41
39
  } else {
42
- consola.info('doctypes/Example.json already exists, skipping')
40
+ consola.info('doctypes/Project.json already exists, skipping')
43
41
  }
44
42
 
45
- // Scaffold example-table.json (table doctype)
46
- const tableExamplePath = join(doctypesDir, 'example-table.json')
47
- if (!existsSync(tableExamplePath)) {
48
- const tableTemplate = await loadTemplate('example-table.json')
49
- await writeFile(tableExamplePath, tableTemplate, 'utf-8')
50
- consola.info('Created doctypes/example-table.json')
43
+ // Scaffold Task.json
44
+ const taskPath = join(doctypesDir, 'Task.json')
45
+ if (!existsSync(taskPath)) {
46
+ const taskTemplate = await loadTemplate('Task.json')
47
+ await writeFile(taskPath, taskTemplate, 'utf-8')
48
+ consola.info('Created doctypes/Task.json')
51
49
  } else {
52
- consola.info('doctypes/example-table.json already exists, skipping')
50
+ consola.info('doctypes/Task.json already exists, skipping')
53
51
  }
54
52
 
55
53
  consola.success('Sample doctypes created successfully')
@@ -59,146 +57,3 @@ export async function installDoctypes(options: DoctypesInstallerOptions): Promis
59
57
  return false
60
58
  }
61
59
  }
62
-
63
- /**
64
- * Load a template file
65
- */
66
- async function loadTemplate(filename: string): Promise<string> {
67
- // Try to load from templates directory
68
- const templatePath = join(__dirname, '..', '..', '..', 'templates', filename)
69
-
70
- if (existsSync(templatePath)) {
71
- return readFile(templatePath, 'utf-8')
72
- }
73
-
74
- // Fallback to inline templates
75
- return getInlineTemplate(filename)
76
- }
77
-
78
- /**
79
- * Get inline template content as fallback
80
- */
81
- function getInlineTemplate(filename: string): string {
82
- const templates: Record<string, string> = {
83
- 'Example.json': JSON.stringify(
84
- {
85
- name: 'Example',
86
- slug: 'example/:id',
87
- fields: [
88
- {
89
- fieldname: 'id',
90
- fieldtype: 'Data',
91
- label: 'ID',
92
- readOnly: true,
93
- },
94
- {
95
- fieldname: 'title',
96
- fieldtype: 'Data',
97
- label: 'Title',
98
- required: true,
99
- },
100
- {
101
- fieldname: 'description',
102
- fieldtype: 'Text',
103
- label: 'Description',
104
- },
105
- {
106
- fieldname: 'status',
107
- fieldtype: 'Select',
108
- label: 'Status',
109
- options: ['Draft', 'Active', 'Archived'],
110
- default: 'Draft',
111
- },
112
- {
113
- fieldname: 'priority',
114
- fieldtype: 'Select',
115
- label: 'Priority',
116
- options: ['Low', 'Medium', 'High'],
117
- default: 'Medium',
118
- },
119
- {
120
- fieldname: 'createdAt',
121
- fieldtype: 'Datetime',
122
- label: 'Created At',
123
- readOnly: true,
124
- },
125
- {
126
- fieldname: 'updatedAt',
127
- fieldtype: 'Datetime',
128
- label: 'Updated At',
129
- readOnly: true,
130
- },
131
- ],
132
- workflow: {
133
- states: ['Draft', 'Active', 'Archived'],
134
- actions: {
135
- activate: {
136
- label: 'Activate',
137
- handler: 'activate_example',
138
- allowedStates: ['Draft'],
139
- confirm: true,
140
- },
141
- archive: {
142
- label: 'Archive',
143
- handler: 'archive_example',
144
- allowedStates: ['Active'],
145
- confirm: true,
146
- },
147
- },
148
- },
149
- },
150
- null,
151
- '\t'
152
- ),
153
- 'example-table.json': JSON.stringify(
154
- {
155
- name: 'Example',
156
- slug: 'example',
157
- schema: [
158
- {
159
- component: 'ATable',
160
- columns: [
161
- {
162
- name: 'id',
163
- label: 'ID',
164
- fieldtype: 'Data',
165
- width: '8ch',
166
- },
167
- {
168
- name: 'title',
169
- label: 'Title',
170
- fieldtype: 'Data',
171
- width: '20ch',
172
- },
173
- {
174
- name: 'status',
175
- label: 'Status',
176
- fieldtype: 'Data',
177
- width: '10ch',
178
- },
179
- {
180
- name: 'priority',
181
- label: 'Priority',
182
- fieldtype: 'Data',
183
- width: '10ch',
184
- },
185
- {
186
- name: 'createdAt',
187
- label: 'Created',
188
- fieldtype: 'Datetime',
189
- width: '18ch',
190
- },
191
- ],
192
- config: {
193
- view: 'list',
194
- },
195
- },
196
- ],
197
- },
198
- null,
199
- '\t'
200
- ),
201
- }
202
-
203
- return templates[filename] || '{}'
204
- }
@@ -3,9 +3,13 @@
3
3
  * Installs @stonecrop/nuxt and configures nuxt.config.ts
4
4
  */
5
5
 
6
+ import { existsSync } from 'node:fs'
7
+ import { mkdir, writeFile, readFile } from 'node:fs/promises'
8
+ import { join } from 'pathe'
6
9
  import consola from 'consola'
7
10
  import { addDependencies } from '../utils/package'
8
11
  import { updateNuxtConfig } from '../utils/config'
12
+ import { loadTemplate } from '../utils/templates'
9
13
 
10
14
  export interface FrontendInstallerOptions {
11
15
  cwd: string
@@ -30,6 +34,7 @@ export async function installFrontend(options: FrontendInstallerOptions): Promis
30
34
  '@stonecrop/node-editor': 'latest',
31
35
  '@stonecrop/schema': 'latest',
32
36
  '@stonecrop/utilities': 'latest',
37
+ '@stonecrop/desktop': 'latest',
33
38
  pinia: '^3.0.4',
34
39
  })
35
40
 
@@ -38,9 +43,17 @@ export async function installFrontend(options: FrontendInstallerOptions): Promis
38
43
  module: "'@stonecrop/nuxt'",
39
44
  moduleOptions: {
40
45
  key: 'stonecrop',
46
+ // routeStrategy registers a single catch-all route that handles both list and
47
+ // detail views; import.meta.url refers to nuxt.config.ts at the project root.
41
48
  value: `{
42
- // Enable DocBuilder for visual schema editing
43
49
  docbuilder: false,
50
+ routeStrategy: () => [
51
+ {
52
+ name: 'stonecrop-catch-all',
53
+ path: '/:pathMatch(.*)*',
54
+ file: new URL('./app/pages/index.vue', import.meta.url).pathname,
55
+ },
56
+ ],
44
57
  }`,
45
58
  },
46
59
  // Add Nitro configuration to handle CSS imports in Stonecrop packages
@@ -54,10 +67,16 @@ export async function installFrontend(options: FrontendInstallerOptions): Promis
54
67
  '@stonecrop/stonecrop',
55
68
  '@stonecrop/node-editor',
56
69
  '@stonecrop/utilities',
70
+ '@stonecrop/desktop',
57
71
  ],
58
72
  },
73
+ css: ['@stonecrop/desktop/styles'],
59
74
  })
60
75
 
76
+ // Scaffold app/composables/useDoctypes.ts, app/composables/useRouteAdapter.ts,
77
+ // app/plugins/stonecrop.client.ts, app/pages/index.vue, and fix app/app.vue
78
+ await scaffoldAppFiles(cwd)
79
+
61
80
  consola.success('@stonecrop/nuxt installed successfully')
62
81
  consola.info('Added Nitro configuration for CSS handling')
63
82
  return true
@@ -66,3 +85,62 @@ export async function installFrontend(options: FrontendInstallerOptions): Promis
66
85
  return false
67
86
  }
68
87
  }
88
+
89
+ /**
90
+ * Scaffold the app directory with Stonecrop client wiring and page component
91
+ */
92
+ async function scaffoldAppFiles(cwd: string): Promise<void> {
93
+ const appDir = join(cwd, 'app')
94
+ const composablesDir = join(appDir, 'composables')
95
+ const pluginsDir = join(appDir, 'plugins')
96
+ const pagesDir = join(appDir, 'pages')
97
+
98
+ for (const dir of [appDir, composablesDir, pluginsDir, pagesDir]) {
99
+ if (!existsSync(dir)) {
100
+ await mkdir(dir, { recursive: true })
101
+ }
102
+ }
103
+
104
+ // Replace nuxi's default app.vue (which renders <NuxtWelcome /> and disables routing)
105
+ // with one that renders <NuxtPage /> so Stonecrop's registered routes are reachable.
106
+ const appVuePath = join(appDir, 'app.vue')
107
+ if (existsSync(appVuePath)) {
108
+ const existing = await readFile(appVuePath, 'utf-8')
109
+ if (existing.includes('NuxtWelcome')) {
110
+ await writeFile(appVuePath, '<template>\n <NuxtPage />\n</template>\n', 'utf-8')
111
+ consola.info('Updated app/app.vue to use <NuxtPage />')
112
+ }
113
+ }
114
+
115
+ const useDoctypesPath = join(composablesDir, 'useDoctypes.ts')
116
+ if (!existsSync(useDoctypesPath)) {
117
+ await writeFile(useDoctypesPath, await loadTemplate('useDoctypes.ts'), 'utf-8')
118
+ consola.info('Created app/composables/useDoctypes.ts')
119
+ } else {
120
+ consola.info('app/composables/useDoctypes.ts already exists, skipping')
121
+ }
122
+
123
+ const useRouteAdapterPath = join(composablesDir, 'useRouteAdapter.ts')
124
+ if (!existsSync(useRouteAdapterPath)) {
125
+ await writeFile(useRouteAdapterPath, await loadTemplate('useRouteAdapter.ts'), 'utf-8')
126
+ consola.info('Created app/composables/useRouteAdapter.ts')
127
+ } else {
128
+ consola.info('app/composables/useRouteAdapter.ts already exists, skipping')
129
+ }
130
+
131
+ const clientPluginPath = join(pluginsDir, 'stonecrop.client.ts')
132
+ if (!existsSync(clientPluginPath)) {
133
+ await writeFile(clientPluginPath, await loadTemplate('stonecrop.client.ts'), 'utf-8')
134
+ consola.info('Created app/plugins/stonecrop.client.ts')
135
+ } else {
136
+ consola.info('app/plugins/stonecrop.client.ts already exists, skipping')
137
+ }
138
+
139
+ const indexPagePath = join(pagesDir, 'index.vue')
140
+ if (!existsSync(indexPagePath)) {
141
+ await writeFile(indexPagePath, await loadTemplate('index.vue'), 'utf-8')
142
+ consola.info('Created app/pages/index.vue')
143
+ } else {
144
+ consola.info('app/pages/index.vue already exists, skipping')
145
+ }
146
+ }
@@ -4,14 +4,12 @@
4
4
  */
5
5
 
6
6
  import { existsSync } from 'node:fs'
7
- import { mkdir, writeFile, readFile } from 'node:fs/promises'
8
- import { join, dirname } from 'pathe'
9
- import { fileURLToPath } from 'node:url'
7
+ import { mkdir, writeFile } from 'node:fs/promises'
8
+ import { join } from 'pathe'
10
9
  import consola from 'consola'
11
10
  import { addDependencies } from '../utils/package'
12
11
  import { updateNuxtConfig } from '../utils/config'
13
-
14
- const __dirname = dirname(fileURLToPath(import.meta.url))
12
+ import { loadTemplate } from '../utils/templates'
15
13
 
16
14
  export interface GrafservInstallerOptions {
17
15
  cwd: string
@@ -52,6 +50,10 @@ export async function installGrafserv(options: GrafservInstallerOptions): Promis
52
50
  moduleOptions: {
53
51
  key: 'grafserv',
54
52
  value: `{
53
+ // Use in-memory Grafast resolvers (no database required)
54
+ // Switch to 'postgraphile' when connecting a real database
55
+ type: 'schema',
56
+
55
57
  // GraphQL schema and resolvers
56
58
  schema: '${schemaPath}',
57
59
  resolvers: '${resolversPath}',
@@ -126,183 +128,29 @@ async function scaffoldServerFiles(cwd: string): Promise<void> {
126
128
  } else {
127
129
  consola.info('server/plugins.ts already exists, skipping')
128
130
  }
129
- }
130
131
 
131
- /**
132
- * Load a template file
133
- */
134
- async function loadTemplate(filename: string): Promise<string> {
135
- // Try to load from templates directory
136
- const templatePath = join(__dirname, '..', '..', '..', 'templates', filename)
137
-
138
- if (existsSync(templatePath)) {
139
- return readFile(templatePath, 'utf-8')
132
+ // Scaffold server/data.ts (in-memory data store)
133
+ const dataPath = join(serverDir, 'data.ts')
134
+ if (!existsSync(dataPath)) {
135
+ const dataTemplate = await loadTemplate('data.ts')
136
+ await writeFile(dataPath, dataTemplate, 'utf-8')
137
+ consola.info('Created server/data.ts')
138
+ } else {
139
+ consola.info('server/data.ts already exists, skipping')
140
140
  }
141
141
 
142
- // Fallback to inline templates
143
- return getInlineTemplate(filename)
144
- }
145
-
146
- /**
147
- * Get inline template content as fallback
148
- */
149
- function getInlineTemplate(filename: string): string {
150
- const templates: Record<string, string> = {
151
- 'schema.graphql': `# Stonecrop GraphQL Schema
152
- # Add your type definitions here
153
-
154
- scalar JSON
155
-
156
- type Query {
157
- """
158
- Health check endpoint
159
- """
160
- healthCheck: HealthStatus!
161
-
162
- """
163
- Get metadata for a doctype
164
- """
165
- getMeta(doctype: String!): JSON
166
- }
167
-
168
- type Mutation {
169
- """
170
- Execute a doctype action
171
- """
172
- stonecropAction(doctype: String!, action: String!, args: JSON): ActionResult!
173
- }
174
-
175
- type HealthStatus {
176
- status: String!
177
- timestamp: String!
178
- }
179
-
180
- type ActionResult {
181
- success: Boolean!
182
- data: JSON
183
- error: String
184
- }
185
- `,
186
- 'resolvers.ts': `/**
187
- * GraphQL Resolvers
188
- * Add your resolver implementations here
189
- */
190
-
191
- export const resolvers = {
192
- Query: {
193
- healthCheck: () => ({
194
- status: 'healthy',
195
- timestamp: new Date().toISOString(),
196
- }),
197
-
198
- getMeta: (_: unknown, { doctype }: { doctype: string }) => {
199
- // TODO: Implement doctype metadata lookup
200
- console.log('getMeta called for:', doctype)
201
- return null
202
- },
203
- },
204
-
205
- Mutation: {
206
- stonecropAction: async (
207
- _: unknown,
208
- { doctype, action, args }: { doctype: string; action: string; args?: unknown }
209
- ) => {
210
- // TODO: Implement action execution
211
- console.log('stonecropAction called:', { doctype, action, args })
212
- return {
213
- success: true,
214
- data: null,
215
- error: null,
216
- }
217
- },
218
- },
219
- }
220
-
221
- export default resolvers
222
- `,
223
- 'plugins.ts': `/**
224
- * Grafserv Plugins
225
- * Add custom middleware and hooks via Grafserv plugins
226
- *
227
- * @see https://grafast.org/grafserv/plugins
228
- */
229
-
230
- import type { GraphileConfig } from 'graphile-config'
231
-
232
- /**
233
- * Example: Request logging plugin
234
- */
235
- const loggingPlugin: GraphileConfig.Plugin = {
236
- name: 'request-logging',
237
- version: '1.0.0',
238
- grafserv: {
239
- middleware: {
240
- processGraphQLRequestBody: async (next, event) => {
241
- const start = Date.now()
242
- console.log('[GraphQL] Request started:', {
243
- path: event.request.url,
244
- method: event.request.method,
245
- })
246
-
247
- const result = await next()
248
-
249
- const duration = Date.now() - start
250
- console.log(\`[GraphQL] Request completed in \${duration}ms\`)
251
-
252
- return result
253
- },
254
- },
255
- },
256
- }
257
-
258
- /**
259
- * Example: Authentication plugin
260
- */
261
- const authPlugin: GraphileConfig.Plugin = {
262
- name: 'authentication',
263
- version: '1.0.0',
264
- grafserv: {
265
- middleware: {
266
- processGraphQLRequestBody: async (next, event) => {
267
- // Extract authentication from headers
268
- const authHeader = event.request.headers.get('authorization')
269
-
270
- if (authHeader?.startsWith('Bearer ')) {
271
- const token = authHeader.slice(7)
272
- // TODO: Validate token and set user context
273
- console.log('[Auth] Token received:', token)
274
- } else {
275
- console.log('[Auth] Anonymous request')
276
- }
277
-
278
- return next()
279
- },
280
- },
281
- },
282
- }
283
-
284
- /**
285
- * Export all plugins
286
- * Import these in your nuxt.config.ts:
287
- *
288
- * import plugins from './server/plugins'
289
- *
290
- * export default defineNuxtConfig({
291
- * grafserv: {
292
- * preset: {
293
- * plugins
294
- * }
295
- * }
296
- * })
297
- */
298
- export const plugins: GraphileConfig.Plugin[] = [
299
- loggingPlugin,
300
- // authPlugin, // Uncomment to enable authentication
301
- ]
302
-
303
- export default plugins
304
- `,
142
+ // Scaffold server/plugins/stonecrop.ts (Nitro server plugin for handler registration)
143
+ const nitroPluginsDir = join(serverDir, 'plugins')
144
+ if (!existsSync(nitroPluginsDir)) {
145
+ await mkdir(nitroPluginsDir, { recursive: true })
146
+ consola.info('Created server/plugins/ directory')
147
+ }
148
+ const stonecropPluginPath = join(nitroPluginsDir, 'stonecrop.ts')
149
+ if (!existsSync(stonecropPluginPath)) {
150
+ const stonecropTemplate = await loadTemplate('stonecrop.ts')
151
+ await writeFile(stonecropPluginPath, stonecropTemplate, 'utf-8')
152
+ consola.info('Created server/plugins/stonecrop.ts')
153
+ } else {
154
+ consola.info('server/plugins/stonecrop.ts already exists, skipping')
305
155
  }
306
-
307
- return templates[filename] || ''
308
156
  }
@@ -21,6 +21,8 @@ export interface NuxtConfigUpdate {
21
21
  nitroConfig?: {
22
22
  externalsInline?: string[]
23
23
  }
24
+ /** CSS files/packages to add to the css array */
25
+ css?: string[]
24
26
  }
25
27
 
26
28
  /**
@@ -224,6 +226,43 @@ export async function updateNuxtConfig(cwd: string, updates: NuxtConfigUpdate):
224
226
  }
225
227
  }
226
228
 
229
+ // Add CSS configuration
230
+ if (updates.css && updates.css.length > 0) {
231
+ const cssMatch = content.match(/css\s*:\s*\[/)
232
+ const newEntries = updates.css.map(pkg => `'${pkg}'`)
233
+
234
+ if (cssMatch && cssMatch.index !== undefined) {
235
+ // css array exists — add entries that aren't present (match regardless of quote style)
236
+ const startIndex = cssMatch.index + cssMatch[0].length
237
+ const closingIndex = findMatchingBracket(content, startIndex - 1)
238
+ if (closingIndex !== -1) {
239
+ const arrayContent = content.slice(startIndex, closingIndex)
240
+ const toAdd = updates.css.filter(
241
+ pkg => !arrayContent.includes(`'${pkg}'`) && !arrayContent.includes(`"${pkg}"`)
242
+ )
243
+ if (toAdd.length > 0) {
244
+ const separator = arrayContent.trim().length > 0 ? ', ' : ''
245
+ const insertion = toAdd.map(pkg => `'${pkg}'`).join(', ')
246
+ content = content.slice(0, closingIndex) + separator + insertion + content.slice(closingIndex)
247
+ modified = true
248
+ }
249
+ }
250
+ } else {
251
+ // No css array — add it after the modules array or at the start of config
252
+ const modulesEndMatch = content.match(/modules\s*:\s*\[[^\]]*\]\s*,?/)
253
+ const defineNuxtConfigMatch = content.match(/defineNuxtConfig\s*\(\s*\{/)
254
+ if (modulesEndMatch && modulesEndMatch.index !== undefined) {
255
+ const insertIndex = modulesEndMatch.index + modulesEndMatch[0].length
256
+ content = content.slice(0, insertIndex) + `\n\n\tcss: [${newEntries.join(', ')}],` + content.slice(insertIndex)
257
+ modified = true
258
+ } else if (defineNuxtConfigMatch && defineNuxtConfigMatch.index !== undefined) {
259
+ const insertIndex = defineNuxtConfigMatch.index + defineNuxtConfigMatch[0].length
260
+ content = content.slice(0, insertIndex) + `\n\tcss: [${newEntries.join(', ')}],` + content.slice(insertIndex)
261
+ modified = true
262
+ }
263
+ }
264
+ }
265
+
227
266
  if (modified) {
228
267
  await writeFile(configPath, content, 'utf-8')
229
268
  consola.success(`Updated ${configPath}`)