pkg-scaffold 3.0.0 → 3.1.1
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/.github/workflows/deploy.yml +55 -0
- package/README.md +62 -52
- package/bin/cli.js +81 -1
- package/docs/.vitepress/config.mts +41 -0
- package/docs/.vitepress/theme/index.ts +17 -0
- package/docs/.vitepress/theme/style.css +139 -0
- package/docs/guide.md +102 -0
- package/docs/index.md +64 -0
- package/docs/reference.md +52 -0
- package/index.js +1 -1
- package/package.json +22 -5
- package/pkg-scaffold/config.json +25 -0
- package/pkg-scaffold/plugins/README.md +19 -0
- package/src/EngineContext.js +40 -3
- package/src/ast/ASTAnalyzer.js +192 -251
- package/src/ast/MagicDetector.js +39 -86
- package/src/ast/OxcAnalyzer.js +114 -0
- package/src/index.js +27 -1
- package/src/performance/GraphCache.js +2 -1
- package/src/performance/SupplyChainGuard.js +41 -55
- package/src/performance/WorkerPool.js +8 -0
- package/src/performance/WorkerTaskRunner.js +7 -2
- package/src/plugins/BasePlugin.js +53 -0
- package/src/plugins/PluginRegistry.js +95 -0
- package/src/plugins/ecosystems/GenericPlugins.js +64 -0
- package/src/plugins/ecosystems/NextJsPlugin.js +33 -0
- package/src/resolution/ConfigLoader.js +59 -0
- package/src/resolution/DependencyProfiler.js +90 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Advanced Plugin Registry supporting Builtin, Custom, and Knip-style plugins.
|
|
7
|
+
*/
|
|
8
|
+
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 };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (this.config.useBuiltinPlugins) {
|
|
25
|
+
await this.loadBuiltinPlugins();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (this.config.useCustomPlugins) {
|
|
29
|
+
await this.loadCustomPlugins(projectRoot);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (this.config.supportKnipPlugins) {
|
|
33
|
+
await this.initKnipAdapter();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async loadBuiltinPlugins() {
|
|
38
|
+
const { NextJsPlugin } = await import('./ecosystems/NextJsPlugin.js');
|
|
39
|
+
const { NuxtPlugin, RemixPlugin, SvelteKitPlugin, AstroPlugin } = await import('./ecosystems/GenericPlugins.js');
|
|
40
|
+
|
|
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
|
+
];
|
|
48
|
+
|
|
49
|
+
builtins.forEach(p => {
|
|
50
|
+
if (!this.config.enabledPlugins || this.config.enabledPlugins.includes(p.name)) {
|
|
51
|
+
this.register(p);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
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));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// No custom plugins or dir missing
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
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
|
+
}
|
|
77
|
+
|
|
78
|
+
register(plugin) {
|
|
79
|
+
this.plugins.set(plugin.name, plugin);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getPlugins() {
|
|
83
|
+
return Array.from(this.plugins.values());
|
|
84
|
+
}
|
|
85
|
+
|
|
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
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return active;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { BasePlugin } from '../BasePlugin.js';
|
|
4
|
+
|
|
5
|
+
export class NuxtPlugin extends BasePlugin {
|
|
6
|
+
get name() { return 'nuxt'; }
|
|
7
|
+
getConfigFiles() { return ['nuxt.config.js', 'nuxt.config.ts']; }
|
|
8
|
+
getRoutePatterns() {
|
|
9
|
+
return [/\/pages\//, /\/server\/(api|routes|middleware)\//, /\/components\/[a-zA-Z0-9_\-\/]+\.vue$/];
|
|
10
|
+
}
|
|
11
|
+
getRequiredSystemContracts() { return ['default']; }
|
|
12
|
+
async isActive(baseDir) {
|
|
13
|
+
for (const file of this.getConfigFiles()) {
|
|
14
|
+
try { await fs.access(path.join(baseDir, file)); return true; } catch {}
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class RemixPlugin extends BasePlugin {
|
|
21
|
+
get name() { return 'remix'; }
|
|
22
|
+
getConfigFiles() { return ['remix.config.js', 'vite.config.js', 'vite.config.ts']; }
|
|
23
|
+
getRoutePatterns() { return [/\/app\/routes\//, /\/app\/root\.(tsx|jsx)$/]; }
|
|
24
|
+
getRequiredSystemContracts() { return ['default', 'loader', 'action', 'meta', 'links']; }
|
|
25
|
+
async isActive(baseDir) {
|
|
26
|
+
for (const file of this.getConfigFiles()) {
|
|
27
|
+
try {
|
|
28
|
+
const content = await fs.readFile(path.join(baseDir, file), 'utf8');
|
|
29
|
+
if (content.includes('@remix-run/') || content.includes('remix')) return true;
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class SvelteKitPlugin extends BasePlugin {
|
|
37
|
+
get name() { return 'sveltekit'; }
|
|
38
|
+
getConfigFiles() { return ['svelte.config.js', 'vite.config.ts']; }
|
|
39
|
+
getRoutePatterns() {
|
|
40
|
+
return [/\+page\.(svelte|ts|js)$/, /\+page\.server\.(ts|js)$/, /\+layout\.(svelte|ts|js)$/, /\+server\.(ts|js)$/];
|
|
41
|
+
}
|
|
42
|
+
getRequiredSystemContracts() { return ['load', 'actions', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH']; }
|
|
43
|
+
async isActive(baseDir) {
|
|
44
|
+
try {
|
|
45
|
+
await fs.access(path.join(baseDir, 'svelte.config.js'));
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class AstroPlugin extends BasePlugin {
|
|
54
|
+
get name() { return 'astro'; }
|
|
55
|
+
getConfigFiles() { return ['astro.config.mjs', 'astro.config.cjs', 'astro.config.ts']; }
|
|
56
|
+
getRoutePatterns() { return [/\/src\/pages\/.*\.astro$/, /\/src\/pages\/.*\.(ts|js)$/]; }
|
|
57
|
+
getRequiredSystemContracts() { return ['default', 'getStaticPaths']; }
|
|
58
|
+
async isActive(baseDir) {
|
|
59
|
+
for (const file of this.getConfigFiles()) {
|
|
60
|
+
try { await fs.access(path.join(baseDir, file)); return true; } catch {}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { BasePlugin } from '../BasePlugin.js';
|
|
4
|
+
|
|
5
|
+
export class NextJsPlugin extends BasePlugin {
|
|
6
|
+
get name() { return 'nextjs'; }
|
|
7
|
+
|
|
8
|
+
getConfigFiles() {
|
|
9
|
+
return ['next.config.js', 'next.config.mjs', 'next.config.ts'];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getRoutePatterns() {
|
|
13
|
+
return [
|
|
14
|
+
/\/pages\/api\//,
|
|
15
|
+
/\/pages\/[a-zA-Z0-9_\-\[\]]+/i,
|
|
16
|
+
/\/app\/([\w\-\[\]]+\/)+(page|route|layout|loading|error|not-found)\.(ts|tsx|js|jsx)$/
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getRequiredSystemContracts() {
|
|
21
|
+
return ['default', 'getServerSideProps', 'getStaticProps', 'getStaticPaths', 'generateMetadata', 'middleware'];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async isActive(baseDir) {
|
|
25
|
+
for (const file of this.getConfigFiles()) {
|
|
26
|
+
try {
|
|
27
|
+
await fs.access(path.join(baseDir, file));
|
|
28
|
+
return true;
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Loads and parses pkg-scaffold configuration files.
|
|
7
|
+
* Supports scaffold.config.js, .scaffoldrc.json, and .scaffoldrc.
|
|
8
|
+
*/
|
|
9
|
+
export class ConfigLoader {
|
|
10
|
+
constructor(context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async loadConfig(projectRoot) {
|
|
15
|
+
const searchPaths = [
|
|
16
|
+
'scaffold.config.js',
|
|
17
|
+
'scaffold.config.mjs',
|
|
18
|
+
'.scaffoldrc.json',
|
|
19
|
+
'.scaffoldrc'
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
for (const fileName of searchPaths) {
|
|
23
|
+
const fullPath = path.join(projectRoot, fileName);
|
|
24
|
+
try {
|
|
25
|
+
await fs.access(fullPath);
|
|
26
|
+
|
|
27
|
+
if (fileName.endsWith('.js') || fileName.endsWith('.mjs')) {
|
|
28
|
+
const module = await import(pathToFileURL(fullPath).href);
|
|
29
|
+
return module.default || module;
|
|
30
|
+
} else {
|
|
31
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return this.getDefaultConfig();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getDefaultConfig() {
|
|
43
|
+
return {
|
|
44
|
+
entryPoints: ['src/index.ts', 'index.js'],
|
|
45
|
+
exclude: [
|
|
46
|
+
'node_modules/**',
|
|
47
|
+
'dist/**',
|
|
48
|
+
'**/*.test.ts',
|
|
49
|
+
'**/*.spec.ts'
|
|
50
|
+
],
|
|
51
|
+
plugins: [],
|
|
52
|
+
rules: {
|
|
53
|
+
'no-unused-exports': 'error',
|
|
54
|
+
'no-unused-vars': 'warn',
|
|
55
|
+
'no-dead-code': 'error'
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Advanced Dependency Profiling Engine.
|
|
6
|
+
* Traces Peer Dependencies and Implicit Tooling Invocations.
|
|
7
|
+
*/
|
|
8
|
+
export class DependencyProfiler {
|
|
9
|
+
constructor(context) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.binaryToPackageMap = {
|
|
12
|
+
'tsc': 'typescript',
|
|
13
|
+
'jest': 'jest',
|
|
14
|
+
'vitest': 'vitest',
|
|
15
|
+
'eslint': 'eslint',
|
|
16
|
+
'prettier': 'prettier',
|
|
17
|
+
'vite': 'vite',
|
|
18
|
+
'next': 'next',
|
|
19
|
+
'nuxt': 'nuxt',
|
|
20
|
+
'astro': 'astro',
|
|
21
|
+
'playwright': 'playwright',
|
|
22
|
+
'cypress': 'cypress',
|
|
23
|
+
'tailwind': 'tailwindcss',
|
|
24
|
+
'postcss': 'postcss'
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Scans package.json scripts and CI files for binary usage.
|
|
30
|
+
*/
|
|
31
|
+
async traceImplicitInvocations(projectRoot) {
|
|
32
|
+
const usedPackages = new Set();
|
|
33
|
+
|
|
34
|
+
// 1. Scan package.json scripts
|
|
35
|
+
try {
|
|
36
|
+
const pkgJsonPath = path.join(projectRoot, 'package.json');
|
|
37
|
+
const pkg = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'));
|
|
38
|
+
|
|
39
|
+
if (pkg.scripts) {
|
|
40
|
+
for (const script of Object.values(pkg.scripts)) {
|
|
41
|
+
this.extractPackagesFromScript(script, usedPackages);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {}
|
|
45
|
+
|
|
46
|
+
// 2. Scan CI workflows
|
|
47
|
+
try {
|
|
48
|
+
const githubWorkflows = path.join(projectRoot, '.github/workflows');
|
|
49
|
+
const files = await fs.readdir(githubWorkflows).catch(() => []);
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
if (file.endsWith('.yml') || file.endsWith('.yaml')) {
|
|
52
|
+
const content = await fs.readFile(path.join(githubWorkflows, file), 'utf8');
|
|
53
|
+
this.extractPackagesFromScript(content, usedPackages);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch (e) {}
|
|
57
|
+
|
|
58
|
+
return usedPackages;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
extractPackagesFromScript(script, collector) {
|
|
62
|
+
// Basic regex to find binary-like words
|
|
63
|
+
const words = script.split(/[\s&|;]+/);
|
|
64
|
+
for (const word of words) {
|
|
65
|
+
const cleanWord = word.replace(/['"]/g, '');
|
|
66
|
+
if (this.binaryToPackageMap[cleanWord]) {
|
|
67
|
+
collector.add(this.binaryToPackageMap[cleanWord]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolves peer dependencies for a given set of used packages.
|
|
74
|
+
*/
|
|
75
|
+
async resolvePeerDependencies(usedPackages, projectRoot) {
|
|
76
|
+
const peerDeps = new Set();
|
|
77
|
+
const nodeModules = path.join(projectRoot, 'node_modules');
|
|
78
|
+
|
|
79
|
+
for (const pkgName of usedPackages) {
|
|
80
|
+
try {
|
|
81
|
+
const pkgJsonPath = path.join(nodeModules, pkgName, 'package.json');
|
|
82
|
+
const pkg = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'));
|
|
83
|
+
if (pkg.peerDependencies) {
|
|
84
|
+
Object.keys(pkg.peerDependencies).forEach(dep => peerDeps.add(dep));
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {}
|
|
87
|
+
}
|
|
88
|
+
return peerDeps;
|
|
89
|
+
}
|
|
90
|
+
}
|