@nordcraft/search 1.0.89 → 1.0.90

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.
Files changed (40) hide show
  1. package/dist/rules/issues/actions/noReferenceProjectActionRule.js +2 -27
  2. package/dist/rules/issues/actions/noReferenceProjectActionRule.js.map +1 -1
  3. package/dist/rules/issues/actions/projectActionIsReferenced.memo.d.ts +3 -0
  4. package/dist/rules/issues/actions/projectActionIsReferenced.memo.js +38 -0
  5. package/dist/rules/issues/actions/projectActionIsReferenced.memo.js.map +1 -0
  6. package/dist/rules/issues/attributes/noReferenceAttributeInInstanceRule.js +2 -2
  7. package/dist/rules/issues/attributes/noReferenceAttributeInInstanceRule.js.map +1 -1
  8. package/dist/rules/issues/components/componentIsReferenced.memo.d.ts +3 -0
  9. package/dist/rules/issues/components/componentIsReferenced.memo.js +25 -0
  10. package/dist/rules/issues/components/componentIsReferenced.memo.js.map +1 -0
  11. package/dist/rules/issues/components/noReferenceComponentRule.d.ts +1 -3
  12. package/dist/rules/issues/components/noReferenceComponentRule.js +8 -26
  13. package/dist/rules/issues/components/noReferenceComponentRule.js.map +1 -1
  14. package/dist/rules/issues/formulas/noReferenceProjectFormulaRule.js +2 -85
  15. package/dist/rules/issues/formulas/noReferenceProjectFormulaRule.js.map +1 -1
  16. package/dist/rules/issues/formulas/projectFormulaIsReferenced.memo.d.ts +3 -0
  17. package/dist/rules/issues/formulas/projectFormulaIsReferenced.memo.js +84 -0
  18. package/dist/rules/issues/formulas/projectFormulaIsReferenced.memo.js.map +1 -0
  19. package/dist/rules/issues/miscellaneous/miscRules.index.js +2 -0
  20. package/dist/rules/issues/miscellaneous/miscRules.index.js.map +1 -1
  21. package/dist/rules/issues/miscellaneous/noReferencePackageRule.d.ts +5 -0
  22. package/dist/rules/issues/miscellaneous/noReferencePackageRule.js +54 -0
  23. package/dist/rules/issues/miscellaneous/noReferencePackageRule.js.map +1 -0
  24. package/dist/searchProject.js +23 -0
  25. package/dist/searchProject.js.map +1 -1
  26. package/dist/types.d.ts +10 -4
  27. package/package.json +3 -3
  28. package/src/rules/issues/actions/noReferenceProjectActionRule.ts +2 -32
  29. package/src/rules/issues/actions/projectActionIsReferenced.memo.ts +52 -0
  30. package/src/rules/issues/attributes/noReferenceAttributeInInstanceRule.ts +2 -2
  31. package/src/rules/issues/components/componentIsReferenced.memo.ts +39 -0
  32. package/src/rules/issues/components/noReferenceComponentRule.ts +10 -39
  33. package/src/rules/issues/formulas/noReferenceProjectFormulaRule.test.ts +33 -0
  34. package/src/rules/issues/formulas/noReferenceProjectFormulaRule.ts +2 -102
  35. package/src/rules/issues/formulas/projectFormulaIsReferenced.memo.ts +112 -0
  36. package/src/rules/issues/miscellaneous/miscRules.index.ts +2 -0
  37. package/src/rules/issues/miscellaneous/noReferencePackageRule.test.ts +289 -0
  38. package/src/rules/issues/miscellaneous/noReferencePackageRule.ts +72 -0
  39. package/src/searchProject.ts +24 -0
  40. package/src/types.ts +11 -0
@@ -0,0 +1,112 @@
1
+ import { ToddleComponent } from '@nordcraft/core/dist/component/ToddleComponent'
2
+ import {
3
+ isToddleFormula,
4
+ type Formula,
5
+ } from '@nordcraft/core/dist/formula/formula'
6
+ import { isDefined } from '@nordcraft/core/dist/utils/util'
7
+ import type { ProjectFiles } from '@nordcraft/ssr/dist/ssr.types'
8
+ import { ToddleApiService } from '@nordcraft/ssr/dist/ToddleApiService'
9
+ import { ToddleRoute } from '@nordcraft/ssr/dist/ToddleRoute'
10
+ import type { MemoFn } from '../../../types'
11
+
12
+ export const projectFormulaIsReferenced = (
13
+ files: Omit<ProjectFiles, 'config'> & Partial<Pick<ProjectFiles, 'config'>>,
14
+ memo: MemoFn,
15
+ ) => {
16
+ const allUsedFormulaKeys = memo('all-used-formulas', () => {
17
+ const usedFormulas = new Set<string>()
18
+
19
+ // Check in all API services first, since that should be quick
20
+ for (const apiService of Object.values(files.services ?? {})) {
21
+ const service = new ToddleApiService({
22
+ service: apiService,
23
+ globalFormulas: { formulas: files.formulas, packages: files.packages },
24
+ })
25
+ const formulas = service.formulasInService()
26
+ for (const { path: _formulaPath, formula } of formulas) {
27
+ // Check if the formula is used in the formula
28
+ checkFormula(usedFormulas, formula)
29
+ }
30
+ }
31
+
32
+ // Check routes before components, since they should be quicker
33
+ for (const projectRoute of Object.values(files.routes ?? {})) {
34
+ const route = new ToddleRoute({
35
+ route: projectRoute,
36
+ globalFormulas: { formulas: files.formulas, packages: files.packages },
37
+ })
38
+ const formulas = route.formulasInRoute()
39
+ for (const { path: _formulaPath, formula } of formulas) {
40
+ // Check if the formula is used in the formula
41
+ checkFormula(usedFormulas, formula)
42
+ }
43
+ }
44
+
45
+ for (const component of Object.values(files.components)) {
46
+ const c = new ToddleComponent({
47
+ // Enforce that the component is not undefined since we're iterating
48
+ component: component!,
49
+ getComponent: (name) => files.components[name],
50
+ packageName: undefined,
51
+ globalFormulas: {
52
+ formulas: files.formulas,
53
+ packages: files.packages,
54
+ },
55
+ })
56
+ for (const { formula } of c.formulasInComponent()) {
57
+ if (formula.type === 'function') {
58
+ usedFormulas.add(
59
+ [formula.package, formula.name].filter(isDefined).join('/'),
60
+ )
61
+ }
62
+ }
63
+ }
64
+
65
+ for (const f of Object.values(files.formulas ?? {})) {
66
+ // Check if the formula is used in the formula
67
+ if (isToddleFormula(f)) {
68
+ const usedFormulasMinusSelf = new Set<string>()
69
+ checkFormula(usedFormulasMinusSelf, f.formula)
70
+ // Add all minus self (a formula is unused, if it's only used by itself)
71
+ usedFormulasMinusSelf.delete(f.name)
72
+ usedFormulasMinusSelf.forEach((f) => usedFormulas.add(f))
73
+ }
74
+ }
75
+
76
+ return usedFormulas
77
+ })
78
+
79
+ return (formulaName: string, packageName?: string) => {
80
+ return allUsedFormulaKeys.has(
81
+ [packageName, formulaName].filter(isDefined).join('/'),
82
+ )
83
+ }
84
+ }
85
+
86
+ const checkArguments = (
87
+ usedFormulas: Set<string>,
88
+ args: {
89
+ formula: Formula
90
+ }[],
91
+ ) => {
92
+ args.forEach((a) => {
93
+ checkFormula(usedFormulas, a.formula)
94
+ })
95
+ }
96
+
97
+ const checkFormula = (usedFormulas: Set<string>, formula: Formula) => {
98
+ if (formula.type === 'function') {
99
+ usedFormulas.add(
100
+ [formula.package, formula.name].filter(isDefined).join('/'),
101
+ )
102
+ checkArguments(usedFormulas, formula.arguments ?? [])
103
+ } else if (
104
+ formula.type === 'object' ||
105
+ formula.type === 'array' ||
106
+ formula.type === 'or' ||
107
+ formula.type === 'and' ||
108
+ formula.type === 'apply'
109
+ ) {
110
+ checkArguments(usedFormulas, formula.arguments ?? [])
111
+ }
112
+ }
@@ -1,5 +1,6 @@
1
1
  import { createStaticSizeConstraintRule } from './createStaticSizeConstraintRule'
2
2
  import { noReferenceNodeRule } from './noReferenceNodeRule'
3
+ import { noReferenceProjectPackageRule } from './noReferencePackageRule'
3
4
  import { requireExtensionRule } from './requireExtensionRule'
4
5
  import { unknownCookieRule } from './unknownCookieRule'
5
6
 
@@ -11,4 +12,5 @@ export default [
11
12
  createStaticSizeConstraintRule('svg', 100 * 1024),
12
13
  // 50 KB is a large img element (with potential base64 encoded image)
13
14
  createStaticSizeConstraintRule('img', 50 * 1024),
15
+ noReferenceProjectPackageRule,
14
16
  ]
@@ -0,0 +1,289 @@
1
+ import type { ProjectFiles } from '@nordcraft/ssr/src/ssr.types'
2
+ import { describe, expect, test } from 'bun:test'
3
+ import { fixProject } from '../../../fixProject'
4
+ import { searchProject } from '../../../searchProject'
5
+ import { noReferenceProjectPackageRule } from './noReferencePackageRule'
6
+
7
+ describe('noReferenceProjectPackageRule', () => {
8
+ test('should report when no formula, actions or components from a package is used', () => {
9
+ const problems = Array.from(
10
+ searchProject({
11
+ files: {
12
+ formulas: {},
13
+ components: {},
14
+ packages: {
15
+ 'my-pkg': {
16
+ manifest: { name: 'my-pkg', commit: 'abc' },
17
+ formulas: {
18
+ f1: {
19
+ name: 'f1',
20
+ exported: true,
21
+ arguments: [],
22
+ formula: { type: 'value', value: 1 },
23
+ },
24
+ },
25
+ actions: {
26
+ a1: {
27
+ name: 'a1',
28
+ version: 2,
29
+ exported: true,
30
+ handler: () => {},
31
+ variableArguments: false,
32
+ },
33
+ },
34
+ components: {
35
+ c1: {
36
+ name: 'c1',
37
+ exported: true,
38
+ nodes: {
39
+ root: {
40
+ type: 'element',
41
+ tag: 'div',
42
+ children: [],
43
+ attrs: {},
44
+ style: {},
45
+ events: {},
46
+ classes: {},
47
+ },
48
+ },
49
+ },
50
+ },
51
+ },
52
+ },
53
+ },
54
+ rules: [noReferenceProjectPackageRule],
55
+ }),
56
+ )
57
+
58
+ expect(problems).toHaveLength(1)
59
+ expect(problems[0].path).toEqual(['packages', 'my-pkg'])
60
+ })
61
+
62
+ test('should not report if just one formula is used', () => {
63
+ const problems = Array.from(
64
+ searchProject({
65
+ files: {
66
+ formulas: {},
67
+ components: {
68
+ comp: {
69
+ name: 'comp',
70
+ nodes: {
71
+ root: {
72
+ type: 'element',
73
+ tag: 'div',
74
+ children: [],
75
+ attrs: {},
76
+ style: {},
77
+ events: {
78
+ onClick: {
79
+ trigger: 'click',
80
+ actions: [
81
+ {
82
+ type: 'SetVariable',
83
+ variable: 'v1',
84
+ data: {
85
+ type: 'function',
86
+ name: 'f1',
87
+ package: 'my-pkg',
88
+ arguments: [],
89
+ },
90
+ },
91
+ ],
92
+ },
93
+ },
94
+ classes: {},
95
+ },
96
+ },
97
+ },
98
+ },
99
+ packages: {
100
+ 'my-pkg': {
101
+ manifest: { name: 'my-pkg', commit: 'abc' },
102
+ formulas: {
103
+ f1: {
104
+ name: 'f1',
105
+ exported: true,
106
+ arguments: [],
107
+ formula: { type: 'value', value: 1 },
108
+ },
109
+ },
110
+ actions: {},
111
+ components: {},
112
+ },
113
+ },
114
+ },
115
+ rules: [noReferenceProjectPackageRule],
116
+ }),
117
+ )
118
+
119
+ expect(problems).toBeEmpty()
120
+ })
121
+
122
+ test('should not report if just one action is used', () => {
123
+ const problems = Array.from(
124
+ searchProject({
125
+ files: {
126
+ formulas: {},
127
+ components: {
128
+ comp: {
129
+ name: 'comp',
130
+ nodes: {
131
+ root: {
132
+ type: 'element',
133
+ tag: 'button',
134
+ children: [],
135
+ attrs: {},
136
+ style: {},
137
+ events: {
138
+ click: {
139
+ trigger: 'click',
140
+ actions: [
141
+ {
142
+ type: 'Custom',
143
+ name: 'a1',
144
+ package: 'my-pkg',
145
+ arguments: [],
146
+ },
147
+ ],
148
+ },
149
+ },
150
+ classes: {},
151
+ },
152
+ },
153
+ },
154
+ },
155
+ packages: {
156
+ 'my-pkg': {
157
+ manifest: { name: 'my-pkg', commit: 'abc' },
158
+ formulas: {},
159
+ actions: {
160
+ a1: {
161
+ name: 'a1',
162
+ version: 2,
163
+ exported: true,
164
+ handler: () => {},
165
+ variableArguments: false,
166
+ },
167
+ },
168
+ components: {},
169
+ },
170
+ },
171
+ },
172
+ rules: [noReferenceProjectPackageRule],
173
+ }),
174
+ )
175
+
176
+ expect(problems).toHaveLength(0)
177
+ })
178
+
179
+ test('should not report if just one component is used', () => {
180
+ const problems = Array.from(
181
+ searchProject({
182
+ files: {
183
+ formulas: {},
184
+ components: {
185
+ main: {
186
+ name: 'main',
187
+ nodes: {
188
+ root: {
189
+ type: 'component',
190
+ name: 'c1',
191
+ package: 'my-pkg',
192
+ attrs: {},
193
+ children: [],
194
+ events: {},
195
+ },
196
+ },
197
+ },
198
+ },
199
+ packages: {
200
+ 'my-pkg': {
201
+ manifest: { name: 'my-pkg', commit: 'abc' },
202
+ formulas: {},
203
+ actions: {},
204
+ components: {
205
+ c1: {
206
+ name: 'c1',
207
+ exported: true,
208
+ nodes: {
209
+ root: {
210
+ type: 'element',
211
+ tag: 'div',
212
+ children: [],
213
+ attrs: {},
214
+ style: {},
215
+ events: {},
216
+ classes: {},
217
+ },
218
+ },
219
+ },
220
+ },
221
+ },
222
+ },
223
+ },
224
+ rules: [noReferenceProjectPackageRule],
225
+ }),
226
+ )
227
+
228
+ expect(problems).toHaveLength(0)
229
+ })
230
+
231
+ test('should report when a package has no components, actions or formulas', () => {
232
+ const problems = Array.from(
233
+ searchProject({
234
+ files: {
235
+ formulas: {},
236
+ components: {},
237
+ packages: {
238
+ 'empty-pkg': {
239
+ manifest: { name: 'empty-pkg', commit: 'abc' },
240
+ formulas: {},
241
+ actions: {},
242
+ components: {},
243
+ },
244
+ },
245
+ },
246
+ rules: [noReferenceProjectPackageRule],
247
+ }),
248
+ )
249
+
250
+ expect(problems).toHaveLength(1)
251
+ expect(problems[0].path).toEqual(['packages', 'empty-pkg'])
252
+ })
253
+ })
254
+
255
+ describe('fix noReferenceProjectPackageRule', () => {
256
+ test('should remove unreferenced package', () => {
257
+ const project: ProjectFiles = {
258
+ formulas: {},
259
+ components: {},
260
+ packages: {
261
+ 'my-pkg': {
262
+ manifest: { name: 'my-pkg', commit: 'abc' },
263
+ formulas: {},
264
+ actions: {},
265
+ components: {},
266
+ },
267
+ },
268
+ }
269
+
270
+ const problems = Array.from(
271
+ searchProject({
272
+ files: project,
273
+ rules: [noReferenceProjectPackageRule],
274
+ }),
275
+ )
276
+
277
+ expect(problems).toHaveLength(1)
278
+ expect(problems[0].code).toBe('no-reference project package')
279
+ expect(problems[0].fixes).toEqual(['uninstall-package'])
280
+
281
+ const fixedProject = fixProject({
282
+ files: project,
283
+ rule: noReferenceProjectPackageRule,
284
+ fixType: 'uninstall-package',
285
+ })
286
+
287
+ expect(fixedProject.packages).toEqual({})
288
+ })
289
+ })
@@ -0,0 +1,72 @@
1
+ import type { PluginActionV2 } from '@nordcraft/core/dist/types'
2
+ import type { Rule } from '../../../types'
3
+ import { removeFromPathFix } from '../../../util/removeUnused.fix'
4
+ import { projectActionIsReferenced } from '../actions/projectActionIsReferenced.memo'
5
+ import { componentIsReferenced } from '../components/componentIsReferenced.memo'
6
+ import { projectFormulaIsReferenced } from '../formulas/projectFormulaIsReferenced.memo'
7
+
8
+ export const noReferenceProjectPackageRule: Rule<{ node: string }> = {
9
+ code: 'no-reference project package',
10
+ level: 'info',
11
+ category: 'No References',
12
+ visit: (report, info) => {
13
+ if (info.nodeType !== 'project-package') {
14
+ return
15
+ }
16
+
17
+ const { files, memo, path, value, packageName } = info
18
+
19
+ const exportedFormulas = Object.entries(value.formulas ?? {}).filter(
20
+ ([, formula]) => formula.exported,
21
+ )
22
+ if (exportedFormulas.length > 0) {
23
+ const projectFormulaIsReferencedFn = projectFormulaIsReferenced(
24
+ files,
25
+ memo,
26
+ )
27
+ for (const [, formula] of exportedFormulas) {
28
+ if (projectFormulaIsReferencedFn(formula.name, packageName)) {
29
+ return
30
+ }
31
+ }
32
+ }
33
+
34
+ const exportedActions = Object.entries(value.actions ?? {}).filter(
35
+ ([, action]) => (action as PluginActionV2).exported,
36
+ )
37
+ if (exportedActions.length > 0) {
38
+ const projectActionIsReferencedFn = projectActionIsReferenced(files, memo)
39
+ for (const [, action] of exportedActions) {
40
+ if (projectActionIsReferencedFn(action.name, packageName)) {
41
+ return
42
+ }
43
+ }
44
+ }
45
+
46
+ const exportedComponents = Object.entries(value.components ?? {}).filter(
47
+ ([, component]) => component?.exported,
48
+ )
49
+ if (exportedComponents.length > 0) {
50
+ const componentIsReferencedFn = componentIsReferenced(files, memo)
51
+ for (const [, component] of exportedComponents) {
52
+ if (component && componentIsReferencedFn(component.name, packageName)) {
53
+ return
54
+ }
55
+ }
56
+ }
57
+
58
+ report({
59
+ path,
60
+ info: {
61
+ title: `Unused package`,
62
+ description: `Package is installed, but none of its formulas, actions, or components are used by any other part of the project. The package can safely be uninstalled.`,
63
+ },
64
+ fixes: ['uninstall-package'],
65
+ })
66
+ },
67
+ fixes: {
68
+ 'uninstall-package': removeFromPathFix,
69
+ },
70
+ }
71
+
72
+ export type NoReferenceProjectPackageRuleFix = 'uninstall-package'
@@ -210,6 +210,29 @@ export function* searchProject({
210
210
  state,
211
211
  fixOptions: fixOptions as any,
212
212
  })
213
+
214
+ if (files.packages) {
215
+ for (const key in files.packages) {
216
+ const pkg = files.packages[key]
217
+ if (pkg) {
218
+ yield* visitNode({
219
+ args: {
220
+ nodeType: 'project-package',
221
+ value: pkg,
222
+ packageName: key,
223
+ path: ['packages', key],
224
+ rules,
225
+ files,
226
+ pathsToVisit,
227
+ useExactPaths,
228
+ memo,
229
+ },
230
+ state,
231
+ fixOptions: fixOptions as any,
232
+ })
233
+ }
234
+ }
235
+ }
213
236
  }
214
237
 
215
238
  function visitNode(args: {
@@ -345,6 +368,7 @@ function* visitNode({
345
368
  case 'project-config':
346
369
  case 'project-theme':
347
370
  case 'project-theme-property':
371
+ case 'project-package':
348
372
  case 'style-declaration':
349
373
  case 'style-variable':
350
374
  case 'style-variant':
package/src/types.ts CHANGED
@@ -27,6 +27,7 @@ import type { StyleVariant } from '@nordcraft/core/dist/styling/variantSelector'
27
27
  import type { Nullable, PluginAction } from '@nordcraft/core/dist/types'
28
28
  import type {
29
29
  ApiService,
30
+ InstalledPackage,
30
31
  ProjectFiles,
31
32
  Route,
32
33
  ToddleProject,
@@ -50,6 +51,7 @@ import type { NoReferenceComponentFormulaRuleFix } from './rules/issues/formulas
50
51
  import type { NoReferenceProjectFormulaRuleFix } from './rules/issues/formulas/noReferenceProjectFormulaRule'
51
52
  import type { NoStaticNodeConditionRuleFix } from './rules/issues/logic/noStaticNodeCondition'
52
53
  import type { NoReferenceNodeRuleFix } from './rules/issues/miscellaneous/noReferenceNodeRule'
54
+ import type { NoReferenceProjectPackageRuleFix } from './rules/issues/miscellaneous/noReferencePackageRule'
53
55
  import type { InvalidStyleSyntaxRuleFix } from './rules/issues/style/invalidStyleSyntaxRule'
54
56
  import type { LegacyStyleVariableRuleFix } from './rules/issues/style/legacyStyleVariableRule'
55
57
  import type { AddToThemeFix } from './rules/issues/style/unknownCSSVariable'
@@ -91,6 +93,7 @@ export type Code =
91
93
  | 'no-reference node'
92
94
  | 'no-reference project action'
93
95
  | 'no-reference project formula'
96
+ | 'no-reference project package'
94
97
  | 'no-reference variable'
95
98
  | 'no-static-node-condition'
96
99
  | 'no-unnecessary-condition-falsy'
@@ -376,6 +379,12 @@ export type ProjectConfigNode = {
376
379
  value: unknown
377
380
  } & Base
378
381
 
382
+ export type ProjectPackageNode = {
383
+ nodeType: 'project-package'
384
+ value: InstalledPackage
385
+ packageName: string
386
+ } & Base
387
+
379
388
  export type StyleVariantNode = {
380
389
  nodeType: 'style-variant'
381
390
  value: {
@@ -435,6 +444,7 @@ export type NodeType =
435
444
  | ProjectRoute
436
445
  | ProjectThemeNode
437
446
  | ProjectThemePropertyNode
447
+ | ProjectPackageNode
438
448
  | StyleNode
439
449
  | StyleVariableNode
440
450
  | StyleVariantNode
@@ -463,6 +473,7 @@ export type FixType =
463
473
  | UnknownActionEventRuleFix
464
474
  | UnknownApiServiceRuleFix
465
475
  | UnknownComponentAttributeRuleFix
476
+ | NoReferenceProjectPackageRuleFix
466
477
 
467
478
  interface ReportedIssueInfo {
468
479
  title: string