@sailshq/language-server 0.5.1 → 0.6.0

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/SailsParser.js CHANGED
@@ -1,3 +1,4 @@
1
+ const fsSync = require('fs')
1
2
  const fs = require('fs').promises
2
3
  const path = require('path')
3
4
  const acorn = require('acorn')
@@ -110,12 +111,14 @@ class SailsParser {
110
111
  for (const { routePattern, actionName } of actionsToParse) {
111
112
  const filePath =
112
113
  path.join(actionsRoot, ...actionName.split('/')) + '.js'
113
- const actionInfo = await this.#parseAction(filePath)
114
+ const exists = fsSync.existsSync(filePath)
115
+ const actionInfo = exists ? await this.#parseAction(filePath) : {}
114
116
 
115
117
  routes[routePattern] = {
116
118
  action: {
117
119
  name: actionName,
118
120
  path: filePath,
121
+ exists,
119
122
  ...actionInfo
120
123
  }
121
124
  }
@@ -0,0 +1,65 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+
3
+ const COMMAND_TIMEOUT = 30000
4
+
5
+ module.exports = function createGenerator({
6
+ type,
7
+ diagnosticCode,
8
+ dataKey,
9
+ validationRegex
10
+ }) {
11
+ function isValid(name) {
12
+ return name && validationRegex.test(name)
13
+ }
14
+
15
+ return {
16
+ diagnosticCode,
17
+ command: `sails.generate${type.charAt(0).toUpperCase() + type.slice(1)}`,
18
+
19
+ createCodeAction(diagnostic) {
20
+ const name = diagnostic.data?.[dataKey]
21
+ if (!isValid(name)) return null
22
+
23
+ return {
24
+ title: `Generate ${type} '${name}'`,
25
+ kind: lsp.CodeActionKind.QuickFix,
26
+ diagnostics: [diagnostic],
27
+ isPreferred: true,
28
+ command: {
29
+ title: `Generate ${type} '${name}'`,
30
+ command: this.command,
31
+ arguments: [name]
32
+ }
33
+ }
34
+ },
35
+
36
+ async executeCommand(args, { rootDir, execAsync, connection }) {
37
+ const name = args[0]
38
+ if (!isValid(name)) return
39
+
40
+ if (!rootDir) {
41
+ connection.window.showErrorMessage(
42
+ `Cannot generate ${type}: workspace root not found.`
43
+ )
44
+ return
45
+ }
46
+
47
+ try {
48
+ connection.window.showInformationMessage(
49
+ `Generating ${type} '${name}'...`
50
+ )
51
+ await execAsync(`npx sails generate ${type} ${name}`, {
52
+ cwd: rootDir,
53
+ timeout: COMMAND_TIMEOUT
54
+ })
55
+ connection.window.showInformationMessage(
56
+ `${type.charAt(0).toUpperCase() + type.slice(1)} '${name}' generated successfully.`
57
+ )
58
+ } catch (error) {
59
+ connection.window.showErrorMessage(
60
+ `Failed to generate ${type}: ${error.message}`
61
+ )
62
+ }
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,8 @@
1
+ const createGenerator = require('./create-generator')
2
+
3
+ module.exports = createGenerator({
4
+ type: 'action',
5
+ diagnosticCode: 'action-not-found',
6
+ dataKey: 'actionName',
7
+ validationRegex: /^[a-zA-Z0-9/_-]+$/
8
+ })
@@ -0,0 +1,8 @@
1
+ const createGenerator = require('./create-generator')
2
+
3
+ module.exports = createGenerator({
4
+ type: 'adapter',
5
+ diagnosticCode: null,
6
+ dataKey: null,
7
+ validationRegex: /^[a-zA-Z0-9_-]+$/
8
+ })
@@ -0,0 +1,8 @@
1
+ const createGenerator = require('./create-generator')
2
+
3
+ module.exports = createGenerator({
4
+ type: 'helper',
5
+ diagnosticCode: null, // No diagnostic triggers this - manual command only
6
+ dataKey: null,
7
+ validationRegex: /^[a-zA-Z0-9/_-]+$/
8
+ })
@@ -0,0 +1,8 @@
1
+ const createGenerator = require('./create-generator')
2
+
3
+ module.exports = createGenerator({
4
+ type: 'hook',
5
+ diagnosticCode: null, // No diagnostic triggers this - manual command only
6
+ dataKey: null,
7
+ validationRegex: /^[a-zA-Z0-9_-]+$/
8
+ })
@@ -0,0 +1,8 @@
1
+ const createGenerator = require('./create-generator')
2
+
3
+ module.exports = createGenerator({
4
+ type: 'model',
5
+ diagnosticCode: 'model-not-found',
6
+ dataKey: 'modelName',
7
+ validationRegex: /^[A-Za-z0-9_]+$/
8
+ })
@@ -0,0 +1,8 @@
1
+ const createGenerator = require('./create-generator')
2
+
3
+ module.exports = createGenerator({
4
+ type: 'response',
5
+ diagnosticCode: null,
6
+ dataKey: null,
7
+ validationRegex: /^[a-zA-Z0-9_-]+$/
8
+ })
@@ -0,0 +1,33 @@
1
+ const handlers = [
2
+ require('./generate-action'),
3
+ require('./generate-model'),
4
+ require('./generate-helper'),
5
+ require('./generate-hook'),
6
+ require('./generate-response'),
7
+ require('./generate-adapter')
8
+ ]
9
+
10
+ module.exports = {
11
+ getCommands() {
12
+ return handlers.map((h) => h.command)
13
+ },
14
+
15
+ getCodeActions(params) {
16
+ const actions = []
17
+ for (const diagnostic of params.context.diagnostics) {
18
+ const handler = handlers.find((h) => h.diagnosticCode === diagnostic.code)
19
+ if (handler) {
20
+ const action = handler.createCodeAction(diagnostic)
21
+ if (action) actions.push(action)
22
+ }
23
+ }
24
+ return actions
25
+ },
26
+
27
+ async executeCommand(params, context) {
28
+ const handler = handlers.find((h) => h.command === params.command)
29
+ if (handler) {
30
+ await handler.executeCommand(params.arguments, context)
31
+ }
32
+ }
33
+ }
package/index.js CHANGED
@@ -1,10 +1,16 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
2
  const TextDocument = require('vscode-languageserver-textdocument').TextDocument
3
+ const { exec } = require('child_process')
4
+ const { promisify } = require('util')
5
+ const execAsync = promisify(exec)
3
6
  const SailsParser = require('./SailsParser')
4
7
 
5
8
  // Validators
6
9
  const validateDocument = require('./validators/validate-document')
7
10
 
11
+ // Code Actions
12
+ const codeActions = require('./code-actions')
13
+
8
14
  // Go-to definitions
9
15
  const goToAction = require('./go-to-definitions/go-to-action')
10
16
  const goToView = require('./go-to-definitions/go-to-view')
@@ -50,11 +56,49 @@ connection.onInitialize(async (params) => {
50
56
  definitionProvider: true,
51
57
  completionProvider: {
52
58
  triggerCharacters: ['"', "'", '.', '{', ',', ' ', '\n']
59
+ },
60
+ codeActionProvider: {
61
+ codeActionKinds: [lsp.CodeActionKind.QuickFix]
62
+ },
63
+ executeCommandProvider: {
64
+ commands: codeActions.getCommands()
53
65
  }
54
66
  }
55
67
  }
56
68
  })
57
69
 
70
+ connection.onInitialized(() => {
71
+ // Register for file create/delete notifications in api/ and config/ directories
72
+ connection.client.register(lsp.DidChangeWatchedFilesNotification.type, {
73
+ watchers: [
74
+ { globPattern: '**/api/**/*.js' },
75
+ { globPattern: '**/api/**/*.ejs' },
76
+ { globPattern: '**/config/**/*.js' },
77
+ { globPattern: '**/views/**/*.ejs' },
78
+ { globPattern: '**/assets/js/pages/**/*.{vue,js,ts,jsx,tsx,svelte,html}' }
79
+ ]
80
+ })
81
+ })
82
+
83
+ connection.onDidChangeWatchedFiles(async (params) => {
84
+ // Check if any relevant files were created or deleted
85
+ const hasRelevantChange = params.changes.some(
86
+ (change) =>
87
+ change.type === lsp.FileChangeType.Created ||
88
+ change.type === lsp.FileChangeType.Deleted
89
+ )
90
+
91
+ if (hasRelevantChange) {
92
+ typeMap = await sailsParser.buildTypeMap()
93
+ connection.console.log('Type map updated due to file create/delete.')
94
+
95
+ // Re-validate all open documents
96
+ for (const document of documents.all()) {
97
+ validateDocument(connection, document, typeMap)
98
+ }
99
+ }
100
+ })
101
+
58
102
  documents.onDidOpen((open) => {
59
103
  if (typeMap) {
60
104
  validateDocument(connection, open.document, typeMap)
@@ -170,6 +214,16 @@ connection.onCompletion(async (params) => {
170
214
  return null
171
215
  })
172
216
 
217
+ connection.onCodeAction((params) => codeActions.getCodeActions(params))
218
+
219
+ connection.onExecuteCommand(async (params) => {
220
+ await codeActions.executeCommand(params, {
221
+ rootDir: sailsParser.rootDir,
222
+ execAsync,
223
+ connection
224
+ })
225
+ })
226
+
173
227
  documents.listen(connection)
174
228
  connection.listen()
175
229
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailshq/language-server",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Language Server Protocol server for Sails Language Service",
5
5
  "homepage": "https://sailjs.com",
6
6
  "repository": {
@@ -27,8 +27,5 @@
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public"
30
- },
31
- "devDependencies": {
32
- "@sailshq/language-server": "^0.2.1"
33
30
  }
34
31
  }
@@ -5,24 +5,30 @@ const walk = require('acorn-walk')
5
5
  module.exports = function validateActionExist(document, typeMap) {
6
6
  const diagnostics = []
7
7
 
8
- if (!document.uri.endsWith('routes.js')) return diagnostics
8
+ if (!document.uri.endsWith('config/routes.js')) return diagnostics
9
+
9
10
  const actions = extractActionInfo(document)
10
11
 
11
12
  for (const { action, range } of actions) {
12
13
  if (isUrlOrRedirect(action)) continue
13
- const routeExists = Object.values(typeMap.routes || {}).some(
14
+
15
+ // Find any route entry that references this action and check if it exists
16
+ const routeEntry = Object.values(typeMap.routes || {}).find(
14
17
  (route) => route.action?.name === action
15
18
  )
16
19
 
17
- if (!routeExists) {
18
- diagnostics.push(
19
- lsp.Diagnostic.create(
20
- range,
21
- `'${action}' action does not exist. Please check the name or create it.`,
22
- lsp.DiagnosticSeverity.Error,
23
- 'sails-lsp'
24
- )
20
+ // Action exists if we found a route entry with exists: true
21
+ // If no route entry found, the action was just added and typeMap is stale,
22
+ // so we need to check exists flag which was set when typeMap was built
23
+ if (routeEntry && routeEntry.action?.exists === false) {
24
+ const diagnostic = lsp.Diagnostic.create(
25
+ range,
26
+ `'${action}' action does not exist. Please check the name or create it.`,
27
+ lsp.DiagnosticSeverity.Error,
28
+ 'action-not-found'
25
29
  )
30
+ diagnostic.data = { actionName: action }
31
+ diagnostics.push(diagnostic)
26
32
  }
27
33
  }
28
34
  return diagnostics
@@ -4,6 +4,13 @@ const walk = require('acorn-walk')
4
4
 
5
5
  module.exports = function validateDataType(document, typeMap) {
6
6
  const diagnostics = []
7
+ const documentUri = document.uri
8
+
9
+ // Only validate files in backend directories where data types are relevant
10
+ if (!documentUri.includes('/api/') && !documentUri.includes('/scripts/')) {
11
+ return diagnostics
12
+ }
13
+
7
14
  const text = document.getText()
8
15
 
9
16
  try {
@@ -4,6 +4,13 @@ const walk = require('acorn-walk')
4
4
 
5
5
  module.exports = function validateHelperInputExist(document, typeMap) {
6
6
  const diagnostics = []
7
+ const documentUri = document.uri
8
+
9
+ // Only validate files in backend directories where helpers are accessible
10
+ if (!documentUri.includes('/api/') && !documentUri.includes('/scripts/')) {
11
+ return diagnostics
12
+ }
13
+
7
14
  const text = document.getText()
8
15
 
9
16
  try {
@@ -130,6 +130,13 @@ function validateCriteriaAttributes(
130
130
  */
131
131
  module.exports = function validateModelAttributeExist(document, typeMap) {
132
132
  const diagnostics = []
133
+ const documentUri = document.uri
134
+
135
+ // Only validate files in backend directories where models are accessible
136
+ if (!documentUri.includes('/api/') && !documentUri.includes('/scripts/')) {
137
+ return diagnostics
138
+ }
139
+
133
140
  const text = document.getText()
134
141
 
135
142
  // Build a lowercased model map for robust case-insensitive lookup
@@ -7,6 +7,13 @@ const lsp = require('vscode-languageserver/node')
7
7
  */
8
8
  module.exports = function validateModelExist(document, typeMap) {
9
9
  const diagnostics = []
10
+ const documentUri = document.uri
11
+
12
+ // Only validate files in backend directories where models are accessible
13
+ if (!documentUri.includes('/api/') && !documentUri.includes('/scripts/')) {
14
+ return diagnostics
15
+ }
16
+
10
17
  const text = document.getText()
11
18
  const models = typeMap.models || {}
12
19
  const lowercasedModelMap = {}
@@ -42,17 +49,17 @@ module.exports = function validateModelExist(document, typeMap) {
42
49
  continue
43
50
  }
44
51
  if (!modelExists(modelName)) {
45
- diagnostics.push(
46
- lsp.Diagnostic.create(
47
- lsp.Range.create(
48
- document.positionAt(match.index),
49
- document.positionAt(match.index + modelName.length)
50
- ),
51
- `Model '${modelName}' not found. Make sure it exists under your api/models directory.`,
52
- lsp.DiagnosticSeverity.Error,
53
- 'sails-lsp'
54
- )
52
+ const diagnostic = lsp.Diagnostic.create(
53
+ lsp.Range.create(
54
+ document.positionAt(match.index),
55
+ document.positionAt(match.index + modelName.length)
56
+ ),
57
+ `Model '${modelName}' not found. Make sure it exists under your api/models directory.`,
58
+ lsp.DiagnosticSeverity.Error,
59
+ 'model-not-found'
55
60
  )
61
+ diagnostic.data = { modelName }
62
+ diagnostics.push(diagnostic)
56
63
  }
57
64
  }
58
65
  // sails.models.user.find() or sails.models.User.find()
@@ -61,19 +68,19 @@ module.exports = function validateModelExist(document, typeMap) {
61
68
  while ((match = sailsModelCallRegex.exec(text)) !== null) {
62
69
  const modelName = match[1]
63
70
  if (!modelExistsLowercased(modelName)) {
64
- diagnostics.push(
65
- lsp.Diagnostic.create(
66
- lsp.Range.create(
67
- document.positionAt(match.index + 'sails.models.'.length),
68
- document.positionAt(
69
- match.index + 'sails.models.'.length + modelName.length
70
- )
71
- ),
72
- `Model '${modelName}' does not exist in this Sails project.`,
73
- lsp.DiagnosticSeverity.Error,
74
- 'sails-lsp'
75
- )
71
+ const diagnostic = lsp.Diagnostic.create(
72
+ lsp.Range.create(
73
+ document.positionAt(match.index + 'sails.models.'.length),
74
+ document.positionAt(
75
+ match.index + 'sails.models.'.length + modelName.length
76
+ )
77
+ ),
78
+ `Model '${modelName}' does not exist in this Sails project.`,
79
+ lsp.DiagnosticSeverity.Error,
80
+ 'model-not-found'
76
81
  )
82
+ diagnostic.data = { modelName }
83
+ diagnostics.push(diagnostic)
77
84
  }
78
85
  }
79
86
  return diagnostics
@@ -2,6 +2,12 @@ const lsp = require('vscode-languageserver/node')
2
2
 
3
3
  module.exports = function validatePageExist(document, typeMap) {
4
4
  const diagnostics = []
5
+ const documentUri = document.uri
6
+
7
+ // Only validate files in api/ where Inertia pages are referenced
8
+ if (!documentUri.includes('/api/')) {
9
+ return diagnostics
10
+ }
5
11
 
6
12
  const pages = extractPageReferences(document)
7
13
  for (const { page, range } of pages) {
@@ -4,6 +4,13 @@ const walk = require('acorn-walk')
4
4
 
5
5
  module.exports = function validateRequiredHelperInput(document, typeMap) {
6
6
  const diagnostics = []
7
+ const documentUri = document.uri
8
+
9
+ // Only validate files in backend directories where helpers are accessible
10
+ if (!documentUri.includes('/api/') && !documentUri.includes('/scripts/')) {
11
+ return diagnostics
12
+ }
13
+
7
14
  const text = document.getText()
8
15
 
9
16
  try {
@@ -2,6 +2,13 @@ const lsp = require('vscode-languageserver/node')
2
2
 
3
3
  module.exports = function validateRequiredModelAttribute(document, typeMap) {
4
4
  const diagnostics = []
5
+ const documentUri = document.uri
6
+
7
+ // Only validate files in backend directories where models are accessible
8
+ if (!documentUri.includes('/api/') && !documentUri.includes('/scripts/')) {
9
+ return diagnostics
10
+ }
11
+
5
12
  const text = document.getText()
6
13
  const models = typeMap.models || {}
7
14