@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 +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/package.json +9 -9
- package/src/cli/index.ts +4 -3
- package/src/cli/installers/doctypes.ts +17 -162
- package/src/cli/installers/frontend.ts +79 -1
- package/src/cli/installers/grafserv.ts +28 -180
- package/src/cli/utils/config.ts +39 -0
- package/src/cli/utils/templates.ts +34 -0
- package/templates/Project.json +56 -0
- package/templates/Task.json +74 -0
- package/templates/data.ts +90 -0
- package/templates/index.vue +98 -0
- package/templates/resolvers.ts +222 -205
- package/templates/schema.graphql +31 -4
- package/templates/stonecrop.client.ts +42 -0
- package/templates/stonecrop.ts +82 -0
- package/templates/useDoctypes.ts +53 -0
- package/templates/useRouteAdapter.ts +36 -0
- package/templates/Example.json +0 -84
- package/templates/example-table.json +0 -58
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
|
|
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
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(
|
|
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.
|
|
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/
|
|
48
|
-
"@stonecrop/casl-middleware": "0.13.
|
|
49
|
-
"@stonecrop/
|
|
50
|
-
"@stonecrop/
|
|
51
|
-
"@stonecrop/graphql-middleware": "0.13.
|
|
52
|
-
"@stonecrop/nuxt-grafserv": "0.13.
|
|
53
|
-
"@stonecrop/stonecrop": "0.13.
|
|
54
|
-
"@stonecrop/schema": "0.13.
|
|
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}.
|
|
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/
|
|
165
|
-
consola.info(` - doctypes/
|
|
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
|
|
8
|
-
import { join
|
|
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
|
|
36
|
-
const
|
|
37
|
-
if (!existsSync(
|
|
38
|
-
const
|
|
39
|
-
await writeFile(
|
|
40
|
-
consola.info('Created doctypes/
|
|
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/
|
|
40
|
+
consola.info('doctypes/Project.json already exists, skipping')
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
// Scaffold
|
|
46
|
-
const
|
|
47
|
-
if (!existsSync(
|
|
48
|
-
const
|
|
49
|
-
await writeFile(
|
|
50
|
-
consola.info('Created doctypes/
|
|
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/
|
|
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
|
|
8
|
-
import { join
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
}
|
package/src/cli/utils/config.ts
CHANGED
|
@@ -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}`)
|