pkg-scaffold 3.1.3 → 3.3.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.
@@ -1,95 +1,128 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs/promises';
3
3
  import { pathToFileURL } from 'url';
4
+ import { KnipAdapter } from './KnipAdapter.js';
4
5
 
5
6
  /**
6
7
  * Advanced Plugin Registry supporting Builtin, Custom, and Knip-style plugins.
8
+ * Version 4.0.0: Enhanced with Modern Frameworks, Backend Services, and Standalone Knip Integration.
7
9
  */
8
10
  export class PluginRegistry {
9
- constructor(context) {
10
- this.context = context;
11
- this.plugins = new Map();
12
- this.config = null;
13
- }
14
-
15
- async init(projectRoot) {
16
- const configPath = path.join(projectRoot, 'pkg-scaffold', 'config.json');
17
- try {
18
- const configRaw = await fs.readFile(configPath, 'utf8');
19
- this.config = JSON.parse(configRaw);
20
- } catch (e) {
21
- this.config = { useBuiltinPlugins: true, useCustomPlugins: true, supportKnipPlugins: true };
11
+ constructor(context) {
12
+ this.context = context;
13
+ this.plugins = new Map();
14
+ this.config = null;
15
+ this.knipAdapter = new KnipAdapter(context);
22
16
  }
23
17
 
24
- if (this.config.useBuiltinPlugins) {
25
- await this.loadBuiltinPlugins();
26
- }
18
+ async init(projectRoot) {
19
+ const configPath = path.join(projectRoot, 'pkg-scaffold', 'config.json');
20
+ try {
21
+ const configRaw = await fs.readFile(configPath, 'utf8');
22
+ this.config = JSON.parse(configRaw);
23
+ } catch (e) {
24
+ this.config = {
25
+ useBuiltinPlugins: true,
26
+ useCustomPlugins: true,
27
+ supportKnipPlugins: true
28
+ };
29
+ }
27
30
 
28
- if (this.config.useCustomPlugins) {
29
- await this.loadCustomPlugins(projectRoot);
30
- }
31
+ if (this.config.useBuiltinPlugins) {
32
+ await this.loadBuiltinPlugins();
33
+ }
34
+
35
+ if (this.config.useCustomPlugins) {
36
+ await this.loadCustomPlugins(projectRoot);
37
+ }
31
38
 
32
- if (this.config.supportKnipPlugins) {
33
- await this.initKnipAdapter();
39
+ if (this.config.supportKnipPlugins) {
40
+ await this.initKnipAdapter(projectRoot);
41
+ }
34
42
  }
35
- }
36
43
 
37
- async loadBuiltinPlugins() {
38
- const { NextJsPlugin } = await import('./ecosystems/NextJsPlugin.js');
39
- const { NuxtPlugin, RemixPlugin, SvelteKitPlugin, AstroPlugin } = await import('./ecosystems/GenericPlugins.js');
44
+ async loadBuiltinPlugins() {
45
+ // Core Ecosystems
46
+ const { NextJsPlugin } = await import('./ecosystems/NextJsPlugin.js');
47
+ const { NuxtPlugin, RemixPlugin, SvelteKitPlugin, AstroPlugin } = await import('./ecosystems/GenericPlugins.js');
48
+ const { TypeScriptPlugin } = await import('./ecosystems/TypeScriptPlugin.js');
49
+
50
+ // Modern Frameworks (New in v4.0)
51
+ const { ReactPlugin, VuePlugin, SveltePlugin, AngularPlugin } = await import('./ecosystems/ModernFrameworks.js');
52
+
53
+ // Backend Services (New in v4.0)
54
+ const { GraphQLPlugin, DatabasePlugin } = await import('./ecosystems/BackendServices.js');
40
55
 
41
- const builtins = [
42
- new NextJsPlugin(this.context),
43
- new NuxtPlugin(this.context),
44
- new RemixPlugin(this.context),
45
- new SvelteKitPlugin(this.context),
46
- new AstroPlugin(this.context)
47
- ];
56
+ const builtins = [
57
+ new NextJsPlugin(this.context),
58
+ new NuxtPlugin(this.context),
59
+ new RemixPlugin(this.context),
60
+ new SvelteKitPlugin(this.context),
61
+ new AstroPlugin(this.context),
62
+ new TypeScriptPlugin(this.context),
63
+ new ReactPlugin(this.context),
64
+ new VuePlugin(this.context),
65
+ new SveltePlugin(this.context),
66
+ new AngularPlugin(this.context),
67
+ new GraphQLPlugin(this.context),
68
+ new DatabasePlugin(this.context)
69
+ ];
48
70
 
49
- builtins.forEach(p => {
50
- if (!this.config.enabledPlugins || this.config.enabledPlugins.includes(p.name)) {
51
- this.register(p);
52
- }
53
- });
54
- }
71
+ builtins.forEach(p => {
72
+ if (!this.config.enabledPlugins || this.config.enabledPlugins.includes(p.name)) {
73
+ this.register(p);
74
+ }
75
+ });
76
+ }
77
+
78
+ async loadCustomPlugins(projectRoot) {
79
+ const pluginsDir = path.join(projectRoot, 'pkg-scaffold', 'plugins');
80
+ try {
81
+ const files = await fs.readdir(pluginsDir);
82
+ for (const file of files) {
83
+ if (file.endsWith('.js') || file.endsWith('.mjs')) {
84
+ const pluginModule = await import(pathToFileURL(path.join(pluginsDir, file)).href);
85
+ const PluginClass = pluginModule.default || pluginModule;
86
+ const pluginInstance = new PluginClass(this.context);
55
87
 
56
- async loadCustomPlugins(projectRoot) {
57
- const pluginsDir = path.join(projectRoot, 'pkg-scaffold', 'plugins');
58
- try {
59
- const files = await fs.readdir(pluginsDir);
60
- for (const file of files) {
61
- if (file.endsWith('.js') || file.endsWith('.mjs')) {
62
- const pluginModule = await import(pathToFileURL(path.join(pluginsDir, file)).href);
63
- const PluginClass = pluginModule.default || pluginModule;
64
- this.register(new PluginClass(this.context));
88
+ const version = pluginInstance.get('version');
89
+ if (version && this.context.verbose) {
90
+ console.log(`[PluginRegistry] Loaded ${pluginInstance.name} v${version}`);
91
+ }
92
+ this.register(pluginInstance);
93
+ }
94
+ }
95
+ } catch (e) {
96
+ // No custom plugins or dir missing
65
97
  }
66
- }
67
- } catch (e) {
68
- // No custom plugins or dir missing
69
98
  }
70
- }
71
99
 
72
- async initKnipAdapter() {
73
- // This adapter allows running Knip-style plugins by wrapping them
74
- // In a real scenario, this would interface with knip's plugin API
75
- this.context.knipCompatible = true;
76
- }
100
+ async initKnipAdapter(projectRoot) {
101
+ this.context.knipCompatible = true;
102
+ await this.knipAdapter.discoverPlugins(projectRoot);
103
+ const knipPlugins = this.knipAdapter.getPlugins();
104
+ knipPlugins.forEach(p => this.register(p));
105
+ }
106
+
107
+ register(plugin) {
108
+ this.plugins.set(plugin.name, plugin);
109
+ }
77
110
 
78
- register(plugin) {
79
- this.plugins.set(plugin.name, plugin);
80
- }
111
+ getPlugins() {
112
+ return Array.from(this.plugins.values());
113
+ }
81
114
 
82
- getPlugins() {
83
- return Array.from(this.plugins.values());
84
- }
115
+ getPlugin(name) {
116
+ return this.plugins.get(name);
117
+ }
85
118
 
86
- async getActivePlugins(baseDir) {
87
- const active = [];
88
- for (const plugin of this.plugins.values()) {
89
- if (await plugin.isActive(baseDir)) {
90
- active.push(plugin);
91
- }
119
+ async getActivePlugins(baseDir) {
120
+ const active = [];
121
+ for (const plugin of this.plugins.values()) {
122
+ if (await plugin.isActive(baseDir)) {
123
+ active.push(plugin);
124
+ }
125
+ }
126
+ return active;
92
127
  }
93
- return active;
94
- }
95
128
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * ============================================================================
3
+ * Backend Services Plugins for pkg-scaffold v4.0.0
4
+ * ============================================================================
5
+ * Built-in support for GraphQL, REST APIs, and Databases.
6
+ */
7
+
8
+ import { BasePlugin } from '../BasePlugin.js';
9
+
10
+ /**
11
+ * GraphQL Ecosystem Plugin
12
+ */
13
+ export class GraphQLPlugin extends BasePlugin {
14
+ get name() {
15
+ return 'graphql';
16
+ }
17
+
18
+ getConfigFiles() {
19
+ return ['package.json', 'graphql.config.js', '.graphqlconfig'];
20
+ }
21
+
22
+ async isActive(baseDir) {
23
+ try {
24
+ const pkgJson = JSON.parse(await require('fs').promises.readFile(require('path').join(baseDir, 'package.json'), 'utf8'));
25
+ return !!(pkgJson.dependencies?.graphql || pkgJson.devDependencies?.graphql || pkgJson.dependencies?.['@apollo/client']);
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ async analyze(node, filePath) {
32
+ // Detect GraphQL tagged templates
33
+ const gqlPattern = /gql\s*`([\s\S]*?)`/g;
34
+ const matches = node.rawCode?.match(gqlPattern) || [];
35
+ if (matches.length > 0) {
36
+ node.graphqlQueries = matches;
37
+ }
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Database Ecosystem Plugin (Prisma, Drizzle, TypeORM)
43
+ */
44
+ export class DatabasePlugin extends BasePlugin {
45
+ get name() {
46
+ return 'database';
47
+ }
48
+
49
+ getConfigFiles() {
50
+ return ['package.json', 'prisma/schema.prisma', 'drizzle.config.ts', 'ormconfig.json'];
51
+ }
52
+
53
+ async analyze(node, filePath) {
54
+ // Detect DB usage
55
+ if (node.explicitImports.has('@prisma/client') || node.explicitImports.has('drizzle-orm')) {
56
+ node.usesDatabase = true;
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * ============================================================================
3
+ * Modern Frameworks Plugins for pkg-scaffold v4.0.0
4
+ * ============================================================================
5
+ * Built-in support for React, Vue, Svelte, and Angular.
6
+ */
7
+
8
+ import { BasePlugin } from '../BasePlugin.js';
9
+
10
+ /**
11
+ * React Ecosystem Plugin
12
+ */
13
+ export class ReactPlugin extends BasePlugin {
14
+ get name() {
15
+ return 'react';
16
+ }
17
+
18
+ getConfigFiles() {
19
+ return ['package.json'];
20
+ }
21
+
22
+ async isActive(baseDir) {
23
+ try {
24
+ const pkgJson = JSON.parse(await require('fs').promises.readFile(require('path').join(baseDir, 'package.json'), 'utf8'));
25
+ return !!(pkgJson.dependencies?.react || pkgJson.devDependencies?.react);
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ getRoutePatterns() {
32
+ return [/\.(tsx?|jsx?)$/];
33
+ }
34
+
35
+ getRequiredSystemContracts() {
36
+ return ['default', 'Component', 'PureComponent', 'Fragment', 'useEffect', 'useState', 'useContext', 'useReducer', 'useCallback', 'useMemo', 'useRef', 'useImperativeHandle', 'useLayoutEffect', 'useDebugValue'];
37
+ }
38
+
39
+ async analyze(node, filePath) {
40
+ if (node.explicitImports.has('react')) {
41
+ node.isReactComponent = true;
42
+ }
43
+
44
+ // Detect JSX
45
+ if (node.rawCode && (node.rawCode.includes('</') || node.rawCode.includes('/>'))) {
46
+ node.hasJSX = true;
47
+ }
48
+
49
+ // Detect Hooks
50
+ const hookMatches = node.rawCode?.match(/use[A-Z]\w+/g) || [];
51
+ if (hookMatches.length > 0) {
52
+ node.reactHooks = new Set(hookMatches);
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Vue Ecosystem Plugin
59
+ */
60
+ export class VuePlugin extends BasePlugin {
61
+ get name() {
62
+ return 'vue';
63
+ }
64
+
65
+ getConfigFiles() {
66
+ return ['package.json', 'vue.config.js', 'vite.config.ts', 'vite.config.js'];
67
+ }
68
+
69
+ async isActive(baseDir) {
70
+ try {
71
+ const pkgJson = JSON.parse(await require('fs').promises.readFile(require('path').join(baseDir, 'package.json'), 'utf8'));
72
+ return !!(pkgJson.dependencies?.vue || pkgJson.devDependencies?.vue);
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ getRoutePatterns() {
79
+ return [/\.vue$/, /\.(tsx?|jsx?)$/];
80
+ }
81
+
82
+ async analyze(node, filePath) {
83
+ if (filePath.endsWith('.vue')) {
84
+ node.isVueSFC = true;
85
+ // Extract template/script/style sections
86
+ const templateMatch = node.rawCode?.match(/<template>([\s\S]*)<\/template>/);
87
+ if (templateMatch) node.vueTemplate = templateMatch[1];
88
+
89
+ const scriptMatch = node.rawCode?.match(/<script(?: setup)?(?: lang=['"]\w+['"])?>([\s\S]*)<\/script>/);
90
+ if (scriptMatch) node.vueScript = scriptMatch[1];
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Svelte Ecosystem Plugin
97
+ */
98
+ export class SveltePlugin extends BasePlugin {
99
+ get name() {
100
+ return 'svelte';
101
+ }
102
+
103
+ getConfigFiles() {
104
+ return ['package.json', 'svelte.config.js'];
105
+ }
106
+
107
+ async isActive(baseDir) {
108
+ try {
109
+ const pkgJson = JSON.parse(await require('fs').promises.readFile(require('path').join(baseDir, 'package.json'), 'utf8'));
110
+ return !!(pkgJson.dependencies?.svelte || pkgJson.devDependencies?.svelte);
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ getRoutePatterns() {
117
+ return [/\.svelte$/, /\.(tsx?|jsx?)$/];
118
+ }
119
+
120
+ async analyze(node, filePath) {
121
+ if (filePath.endsWith('.svelte')) {
122
+ node.isSvelteComponent = true;
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Angular Ecosystem Plugin
129
+ */
130
+ export class AngularPlugin extends BasePlugin {
131
+ get name() {
132
+ return 'angular';
133
+ }
134
+
135
+ getConfigFiles() {
136
+ return ['package.json', 'angular.json'];
137
+ }
138
+
139
+ async isActive(baseDir) {
140
+ try {
141
+ const pkgJson = JSON.parse(await require('fs').promises.readFile(require('path').join(baseDir, 'package.json'), 'utf8'));
142
+ return !!(pkgJson.dependencies?.['@angular/core'] || pkgJson.devDependencies?.['@angular/core']);
143
+ } catch {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ getRoutePatterns() {
149
+ return [/\.ts$/];
150
+ }
151
+
152
+ async analyze(node, filePath) {
153
+ if (node.rawCode?.includes('@Component') || node.rawCode?.includes('@Injectable') || node.rawCode?.includes('@NgModule')) {
154
+ node.isAngularEntity = true;
155
+ }
156
+ }
157
+ }
@@ -0,0 +1,56 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ import { BasePlugin } from '../BasePlugin.js';
4
+
5
+ /**
6
+ * TypeScript Plugin for pkg-scaffold.
7
+ * Handles tsconfig.json detection and TypeScript-specific entry points.
8
+ */
9
+ export class TypeScriptPlugin extends BasePlugin {
10
+ get name() {
11
+ return 'typescript';
12
+ }
13
+
14
+ getConfigFiles() {
15
+ return ['tsconfig.json', 'tsconfig.base.json', 'tsconfig.eslint.json'];
16
+ }
17
+
18
+ getRoutePatterns() {
19
+ // Common TypeScript entry points and declaration files
20
+ return [
21
+ /src\/index\.ts$/,
22
+ /src\/main\.ts$/,
23
+ /src\/lib\.ts$/,
24
+ /.*\.d\.ts$/
25
+ ];
26
+ }
27
+
28
+ getRequiredSystemContracts() {
29
+ // TypeScript specific implicit exports or requirements
30
+ return ['default'];
31
+ }
32
+
33
+ /**
34
+ * Custom Getter for v3.2.0: Get the compiler version from the project.
35
+ */
36
+ async getCompilerVersion() {
37
+ try {
38
+ const packageJsonPath = path.join(this.context.cwd, 'package.json');
39
+ const content = await fs.readFile(packageJsonPath, 'utf8');
40
+ const pkg = JSON.parse(content);
41
+ return pkg.devDependencies?.typescript || pkg.dependencies?.typescript || 'unknown';
42
+ } catch {
43
+ return 'not installed';
44
+ }
45
+ }
46
+
47
+ async isActive(baseDir) {
48
+ for (const file of this.getConfigFiles()) {
49
+ try {
50
+ await fs.access(path.join(baseDir, file));
51
+ return true;
52
+ } catch {}
53
+ }
54
+ return false;
55
+ }
56
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * ============================================================================
3
+ * Circular Dependency Detector for pkg-scaffold v3.3.0
4
+ *
5
+ * Copyright (C) 2026 DreamLongYT
6
+ * Licensed under the Apache License, Version 2.0.
7
+ * "The original code was from the lovely DreamLong"
8
+ * ============================================================================
9
+ * Implements a high-performance Tarjan-based or DFS-based algorithm to
10
+ * detect circular dependencies in the codebase graph.
11
+ * Addresses Knip Issue #1734.
12
+ */
13
+
14
+ export class CircularDetector {
15
+ constructor(context) {
16
+ this.context = context;
17
+ this.cycles = [];
18
+ }
19
+
20
+ /**
21
+ * Detects cycles in the provided dependency graph
22
+ * @param {Map} graph - The codebase dependency graph
23
+ * @returns {Array} List of detected cycles
24
+ */
25
+ detectCycles(graph) {
26
+ this.cycles = [];
27
+ const visited = new Set();
28
+ const stack = new Set();
29
+ const path = [];
30
+
31
+ for (const filePath of graph.keys()) {
32
+ if (!visited.has(filePath)) {
33
+ this.dfs(filePath, graph, visited, stack, path);
34
+ }
35
+ }
36
+
37
+ return this.cycles;
38
+ }
39
+
40
+ dfs(node, graph, visited, stack, path) {
41
+ visited.add(node);
42
+ stack.add(node);
43
+ path.push(node);
44
+
45
+ const edges = graph.get(node)?.outgoingEdges || [];
46
+ for (const neighbor of edges) {
47
+ if (stack.has(neighbor)) {
48
+ // Cycle detected
49
+ const cycleStartIndex = path.indexOf(neighbor);
50
+ const cycle = path.slice(cycleStartIndex);
51
+ this.cycles.push(cycle);
52
+ } else if (!visited.has(neighbor)) {
53
+ this.dfs(neighbor, graph, visited, stack, path);
54
+ }
55
+ }
56
+
57
+ stack.delete(node);
58
+ path.pop();
59
+ }
60
+
61
+ /**
62
+ * Formats cycles for reporting
63
+ */
64
+ formatCycles() {
65
+ return this.cycles.map(cycle => {
66
+ return cycle.join(' -> ') + ' -> ' + cycle[0];
67
+ });
68
+ }
69
+ }
70
+
71
+ export default CircularDetector;
@@ -25,15 +25,25 @@ export class PathMapper {
25
25
  const rawText = await fs.readFile(configPath, 'utf8');
26
26
 
27
27
  // Strip inline single-line and block comments before parsing
28
- const jsonCleanText = rawText.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1');
28
+ // Improved regex to handle more edge cases in tsconfig comments
29
+ const jsonCleanText = rawText
30
+ .replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1')
31
+ .replace(/,(\s*[\]}])/g, '$1'); // Remove trailing commas
32
+
29
33
  const tsconfig = JSON.parse(jsonCleanText);
30
34
 
31
35
  if (!tsconfig.compilerOptions) return;
32
36
 
33
37
  const opts = tsconfig.compilerOptions;
38
+
39
+ // v6 Path Resolution Fix (Knip Issue #1794)
40
+ // Ensure baseUrl is correctly resolved relative to the tsconfig file location
41
+ const configDir = path.dirname(configPath);
34
42
  if (opts.baseUrl) {
35
43
  this.baseUrl = opts.baseUrl;
36
- this.absoluteBaseUrl = path.resolve(this.context.cwd, this.baseUrl);
44
+ this.absoluteBaseUrl = path.resolve(configDir, this.baseUrl);
45
+ } else {
46
+ this.absoluteBaseUrl = configDir;
37
47
  }
38
48
 
39
49
  if (opts.paths) {
@@ -21,6 +21,16 @@ export class WorkspaceGraph {
21
21
  const pnpmWorkspacePath = path.join(this.context.cwd, 'pnpm-workspace.yaml');
22
22
 
23
23
  let workspaceGlobs = [];
24
+ this.hoistedDependencies = new Set();
25
+
26
+ // Load hoisted dependencies from root package.json (Knip Issue #1792 fix)
27
+ try {
28
+ const rootPkg = JSON.parse(await fs.readFile(rootPackageJsonPath, 'utf8'));
29
+ const deps = { ...rootPkg.dependencies, ...rootPkg.devDependencies };
30
+ Object.keys(deps).forEach(d => this.hoistedDependencies.add(d));
31
+ } catch (e) {
32
+ // No root package.json or unreadable
33
+ }
24
34
 
25
35
  // Protocol A: Check for pnpm workspace configurations
26
36
  try {