@africode/core 5.0.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/AFRICODE_FRAMEWORK_GUIDE.md +707 -0
- package/LICENSE +623 -0
- package/README.md +442 -0
- package/bin/africode.js +73 -0
- package/bin/africode.js.1758507140 +343 -0
- package/bin/cli.ts +83 -0
- package/bin/create-africode.js +158 -0
- package/bin/scaffold.ts +219 -0
- package/components/accordion.js +183 -0
- package/components/alert.js +131 -0
- package/components/auth.js +172 -0
- package/components/avatar.js +117 -0
- package/components/badge.js +104 -0
- package/components/base.d.ts +139 -0
- package/components/base.js +184 -0
- package/components/button.js +164 -0
- package/components/card.js +137 -0
- package/components/cultural-card.js +243 -0
- package/components/divider.js +83 -0
- package/components/dropdown.js +171 -0
- package/components/error-boundary.js +155 -0
- package/components/form.js +131 -0
- package/components/grid.js +273 -0
- package/components/hero.js +138 -0
- package/components/icon.js +36 -0
- package/components/index.js +57 -0
- package/components/input.js +256 -0
- package/components/kanga-card.js +185 -0
- package/components/language-switcher.js +108 -0
- package/components/loader.js +80 -0
- package/components/modal.js +262 -0
- package/components/motion.js +84 -0
- package/components/navbar.js +236 -0
- package/components/pattern-showcase.js +225 -0
- package/components/progress.js +134 -0
- package/components/react.js +111 -0
- package/components/section.js +54 -0
- package/components/select.js +322 -0
- package/components/sidebar.js +180 -0
- package/components/skeleton.js +85 -0
- package/components/table.js +181 -0
- package/components/tabs.js +202 -0
- package/components/theme-toggle.js +82 -0
- package/components/toast.js +139 -0
- package/components/tooltip.js +167 -0
- package/core/a2ui-schema-manager.js +344 -0
- package/core/a2ui.js +431 -0
- package/core/bun-runtime.js +799 -0
- package/core/cli/commands/add.js +23 -0
- package/core/cli/commands/audit.js +58 -0
- package/core/cli/commands/build.js +137 -0
- package/core/cli/commands/create-plugin.js +241 -0
- package/core/cli/commands/dev.js +228 -0
- package/core/cli/commands/lint.js +23 -0
- package/core/cli/commands/test.js +34 -0
- package/core/cli/migrator.js +71 -0
- package/core/cli/ui.js +46 -0
- package/core/compliance.js +628 -0
- package/core/config.js +263 -0
- package/core/db-advanced.js +481 -0
- package/core/db.js +284 -0
- package/core/enhanced-hmr.js +404 -0
- package/core/errors.js +222 -0
- package/core/file-router.js +290 -0
- package/core/heartbeat.js +64 -0
- package/core/hmr-client.js +204 -0
- package/core/hmr.js +196 -0
- package/core/html.d.ts +116 -0
- package/core/html.js +160 -0
- package/core/hydration.js +52 -0
- package/core/lipa-namba-journey.js +572 -0
- package/core/motion.js +106 -0
- package/core/nida-cig-middleware.js +455 -0
- package/core/patterns.d.ts +124 -0
- package/core/patterns.js +833 -0
- package/core/plugins/index.js +312 -0
- package/core/router.js +387 -0
- package/core/sdk-client.js +62 -0
- package/core/sdk.d.ts +133 -0
- package/core/sdk.js +123 -0
- package/core/seo.js +76 -0
- package/core/server/auth-endpoints.js +339 -0
- package/core/server/auth.js +180 -0
- package/core/server/csrf.js +206 -0
- package/core/server/db.js +39 -0
- package/core/server/middleware.js +324 -0
- package/core/server/rate-limit.js +238 -0
- package/core/server/render.js +69 -0
- package/core/server/router.js +120 -0
- package/core/shim.js +28 -0
- package/core/state.d.ts +86 -0
- package/core/state.js +242 -0
- package/core/store.d.ts +122 -0
- package/core/store.js +61 -0
- package/core/validation.d.ts +233 -0
- package/core/validation.js +590 -0
- package/core/websocket.js +639 -0
- package/dist/africode.js +2905 -0
- package/dist/africode.js.map +61 -0
- package/dist/build-info.json +23 -0
- package/dist/components.js +2888 -0
- package/dist/components.js.map +58 -0
- package/dist/styles/africanity.css +322 -0
- package/dist/styles/typography.css +141 -0
- package/docs/IDE-Guide.md +50 -0
- package/package.json +110 -0
- package/src/index.ts +196 -0
- package/styles/africanity.css +322 -0
- package/styles/typography.css +141 -0
- package/templates/starter/.env.example +15 -0
- package/templates/starter/africode.config.js +40 -0
- package/templates/starter/package.json +14 -0
- package/templates/starter/src/pages/index.html +46 -0
- package/templates/starter/src/pages/index.js +32 -0
- package/templates/starter/src/styles/main.css +4 -0
- package/templates/starter-3d/.env.example +7 -0
- package/templates/starter-3d/africode.config.js +29 -0
- package/templates/starter-3d/components/af-model-viewer.js +125 -0
- package/templates/starter-3d/package.json +15 -0
- package/templates/starter-3d/src/pages/index.html +46 -0
- package/templates/starter-3d/src/pages/index.js +50 -0
- package/templates/starter-3d/src/styles/main.css +4 -0
- package/templates/starter-react/.env.example +15 -0
- package/templates/starter-react/africode.config.js +40 -0
- package/templates/starter-react/package.json +16 -0
- package/templates/starter-react/src/pages/index.html +46 -0
- package/templates/starter-react/src/pages/index.js +68 -0
- package/templates/starter-react/src/styles/main.css +4 -0
- package/templates/starter-tailwind/.env.example +15 -0
- package/templates/starter-tailwind/africode.config.js +40 -0
- package/templates/starter-tailwind/package.json +20 -0
- package/templates/starter-tailwind/src/pages/index.html +46 -0
- package/templates/starter-tailwind/src/pages/index.js +37 -0
- package/templates/starter-tailwind/src/styles/main.css +4 -0
- package/templates/starter-tailwind/src/styles/tailwind.css +1 -0
- package/templates/starter-tailwind/src/tailwind-loader.js +30 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Add Component Command
|
|
3
|
+
*/
|
|
4
|
+
import { colors } from '../ui.js';
|
|
5
|
+
|
|
6
|
+
export async function addComponent(name) {
|
|
7
|
+
if (!name) {
|
|
8
|
+
console.log(`${colors.red}✗ Please specify a component name${colors.reset}`);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const components = {
|
|
12
|
+
sidebar: 'af-sidebar',
|
|
13
|
+
tooltip: 'af-tooltip',
|
|
14
|
+
toast: 'af-toast',
|
|
15
|
+
skeleton: 'af-skeleton',
|
|
16
|
+
dropdown: 'af-dropdown'
|
|
17
|
+
};
|
|
18
|
+
if (!components[name]) {
|
|
19
|
+
console.log(`${colors.red}✗ Unknown component: ${name}${colors.reset}`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(`${colors.green}✓ Component ${colors.gold}${components[name]}${colors.green} is available in AfriCode!${colors.reset}`);
|
|
23
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
function scanDirectory(dir, issues = []) {
|
|
5
|
+
try {
|
|
6
|
+
const files = readdirSync(dir);
|
|
7
|
+
for (const file of files) {
|
|
8
|
+
const filePath = join(dir, file);
|
|
9
|
+
if (statSync(filePath).isDirectory()) {
|
|
10
|
+
scanDirectory(filePath, issues);
|
|
11
|
+
} else if (file.endsWith('.js') || file.endsWith('.ts') || file.endsWith('.html')) {
|
|
12
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
13
|
+
|
|
14
|
+
// Static heuristic checks
|
|
15
|
+
if (content.includes('<img ') && !content.includes('alt=')) {
|
|
16
|
+
issues.push(`[WARN] ${file}: <img> tag missing 'alt' attribute`);
|
|
17
|
+
}
|
|
18
|
+
if (content.includes('<af-button') && !content.includes('aria-label=') && content.includes('icon=')) {
|
|
19
|
+
issues.push(`[HINT] ${file}: Icon-only button might need 'aria-label'`);
|
|
20
|
+
}
|
|
21
|
+
if (content.includes('<input ') && !content.includes('id=')) {
|
|
22
|
+
issues.push(`[HINT] ${file}: Input missing 'id', cannot link to a <label>`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch(e) {
|
|
27
|
+
// Ignore if dir doesn't exist
|
|
28
|
+
}
|
|
29
|
+
return issues;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function audit() {
|
|
33
|
+
console.log('\n🔍 AfriCode Static Accessibility Hints');
|
|
34
|
+
console.log('Note: This is a static analysis heuristic, NOT a full WCAG certification tool.\n');
|
|
35
|
+
|
|
36
|
+
const targets = [join(process.cwd(), 'components'), join(process.cwd(), 'pages')];
|
|
37
|
+
const allIssues = [];
|
|
38
|
+
|
|
39
|
+
for (const target of targets) {
|
|
40
|
+
scanDirectory(target, allIssues);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (allIssues.length > 0) {
|
|
44
|
+
console.log('⚠️ Potential Concerns:\n');
|
|
45
|
+
allIssues.forEach(issue => console.log(' ' + issue));
|
|
46
|
+
} else {
|
|
47
|
+
console.log('✨ No obvious static A11y issues detected in UI files.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log('\n💡 Best Practices Check:');
|
|
51
|
+
console.log(' - Are all interactive elements reachable by the <Tab> key?');
|
|
52
|
+
console.log(' - Do visual icons have an invisible aria-label text?');
|
|
53
|
+
console.log(' - Are you using role="dialog" and aria-modal="true" for popups?\n');
|
|
54
|
+
|
|
55
|
+
console.log('For comprehensive testing, we recommend:');
|
|
56
|
+
console.log(' 1. Axe DevTools browser extension');
|
|
57
|
+
console.log(' 2. Screen reader manual testing (VoiceOver/TalkBack)');
|
|
58
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Build Command
|
|
3
|
+
*/
|
|
4
|
+
import { logo, colors } from '../ui.js';
|
|
5
|
+
import { rm, cp, mkdir, readdir, writeFile } from 'fs/promises';
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
export async function build() {
|
|
11
|
+
logo();
|
|
12
|
+
console.log(`${colors.green}▶ Building for production (SSG + Islands)...${colors.reset}\n`);
|
|
13
|
+
|
|
14
|
+
const distDir = './dist';
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
const hasSrc = existsSync(path.join(cwd, 'src'));
|
|
17
|
+
const pagesDir = hasSrc ? './src/pages' : './pages';
|
|
18
|
+
const componentsDir = hasSrc ? './src/components' : './components';
|
|
19
|
+
const stylesDir = hasSrc ? './src/styles' : './styles';
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const buildStart = performance.now();
|
|
23
|
+
await rm(distDir, { recursive: true, force: true });
|
|
24
|
+
await mkdir(distDir);
|
|
25
|
+
|
|
26
|
+
console.log(' Bundling JavaScript Islands...');
|
|
27
|
+
const bundleStart = performance.now();
|
|
28
|
+
|
|
29
|
+
// Resolve core paths relative to the framework - Robust for Windows
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
+
const __dirname = path.dirname(__filename);
|
|
32
|
+
const frameworkCoreDir = path.resolve(__dirname, '../../');
|
|
33
|
+
const sdkPath = path.join(frameworkCoreDir, 'sdk.js');
|
|
34
|
+
const projectComponentsPath = path.resolve(cwd, `${componentsDir}/index.js`);
|
|
35
|
+
|
|
36
|
+
// Check if paths exist before building
|
|
37
|
+
if (!await Bun.file(sdkPath).exists()) {throw new Error(`SDK not found at ${sdkPath}`);}
|
|
38
|
+
if (!await Bun.file(projectComponentsPath).exists()) {throw new Error(`Project components not found at ${projectComponentsPath}`);}
|
|
39
|
+
|
|
40
|
+
console.log(` SDK Entry: ${sdkPath}`);
|
|
41
|
+
console.log(` Components Entry: ${projectComponentsPath}`);
|
|
42
|
+
|
|
43
|
+
let buildResult;
|
|
44
|
+
try {
|
|
45
|
+
buildResult = await Bun.build({
|
|
46
|
+
entrypoints: [sdkPath, projectComponentsPath],
|
|
47
|
+
outdir: './dist/assets',
|
|
48
|
+
minify: true,
|
|
49
|
+
splitting: true,
|
|
50
|
+
target: 'browser',
|
|
51
|
+
naming: '[name]-[hash].[ext]'
|
|
52
|
+
});
|
|
53
|
+
} catch (bundleErr) {
|
|
54
|
+
console.error(`${colors.red} ! Bun.build threw an exception:${colors.reset}`, bundleErr);
|
|
55
|
+
throw bundleErr;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Log all build output for debugging
|
|
59
|
+
for (const log of buildResult.logs) {
|
|
60
|
+
console.log(` ${log.level === 'error' ? colors.red : colors.blue} ${log.message}${colors.reset}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const bundleEnd = performance.now();
|
|
64
|
+
console.log(` ${colors.blue}⚡ Bundled in ${(bundleEnd - bundleStart).toFixed(2)}ms${colors.reset}`);
|
|
65
|
+
|
|
66
|
+
if (!buildResult.success) {
|
|
67
|
+
throw new Error('Bundle failed');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(' Pre-rendering Pages (SSG)...');
|
|
71
|
+
const pageFiles = await readdir(pagesDir);
|
|
72
|
+
const routes = [];
|
|
73
|
+
|
|
74
|
+
for (const file of pageFiles) {
|
|
75
|
+
if (!file.endsWith('.js')) {continue;}
|
|
76
|
+
|
|
77
|
+
const name = file.replace('.js', '');
|
|
78
|
+
const route = name === 'index' ? '/' : `/${name}`;
|
|
79
|
+
routes.push(route);
|
|
80
|
+
|
|
81
|
+
const renderStart = performance.now();
|
|
82
|
+
console.log(` • Rendering ${route}...`);
|
|
83
|
+
const pagePath = path.resolve(process.cwd(), pagesDir, file);
|
|
84
|
+
const pageModule = await import(pagePath);
|
|
85
|
+
|
|
86
|
+
let loaderData = {};
|
|
87
|
+
if (typeof pageModule.loader === 'function') {
|
|
88
|
+
loaderData = await pageModule.loader({ request: new Request(`http://localhost${route}`), params: {} });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (typeof pageModule.default === 'function') {
|
|
92
|
+
const htmlContent = pageModule.default({ data: loaderData });
|
|
93
|
+
const outputPath = path.join(distDir, `${name}.html`);
|
|
94
|
+
await writeFile(outputPath, htmlContent);
|
|
95
|
+
const renderEnd = performance.now();
|
|
96
|
+
console.log(` ${colors.blue}⚡ ${name} pre-rendered in ${(renderEnd - renderStart).toFixed(2)}ms${colors.reset}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(' Generating Sitemap...');
|
|
101
|
+
const seoPath = path.join(frameworkCoreDir, 'seo.js');
|
|
102
|
+
const { generateSitemap } = await import(seoPath);
|
|
103
|
+
const sitemapXml = generateSitemap('https://your-domain.com', routes);
|
|
104
|
+
await writeFile(path.join(distDir, 'sitemap.xml'), sitemapXml);
|
|
105
|
+
|
|
106
|
+
console.log(' Copying Assets...');
|
|
107
|
+
if (await Bun.file(stylesDir).exists()) {await cp(stylesDir, `${distDir}/styles`, { recursive: true });}
|
|
108
|
+
|
|
109
|
+
if (await Bun.file('./server.js').exists()) {
|
|
110
|
+
console.log(' Bundling Production Server...');
|
|
111
|
+
await Bun.build({
|
|
112
|
+
entrypoints: ['./server.js'],
|
|
113
|
+
outdir: './dist',
|
|
114
|
+
target: 'bun'
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const buildEnd = performance.now();
|
|
119
|
+
const totalTime = ((buildEnd - buildStart) / 1000).toFixed(2);
|
|
120
|
+
|
|
121
|
+
// Write performance metrics
|
|
122
|
+
await writeFile(path.join(distDir, 'performance.json'), JSON.stringify({
|
|
123
|
+
totalBuildTime: totalTime,
|
|
124
|
+
bundleTime: (bundleEnd - bundleStart).toFixed(2),
|
|
125
|
+
pageCount: routes.length,
|
|
126
|
+
timestamp: new Date().toISOString()
|
|
127
|
+
}, null, 2));
|
|
128
|
+
|
|
129
|
+
console.log(`\n${colors.green}✓ Build complete in ${totalTime}s!${colors.reset}`);
|
|
130
|
+
console.log(` Output: ${colors.blue}./dist${colors.reset} (Ready to deploy)`);
|
|
131
|
+
console.log(` Pages: ${colors.bold}${routes.length}${colors.reset} pre-rendered`);
|
|
132
|
+
console.log(` Sitemap: ${colors.blue}./dist/sitemap.xml${colors.reset}\n`);
|
|
133
|
+
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`${colors.red}✗ Build failed:${colors.reset}`, err.message || err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Plugin Scaffolder
|
|
3
|
+
*
|
|
4
|
+
* Generates a complete, testable, safe plugin project.
|
|
5
|
+
* Usage: africode create plugin my-plugin [--minimal] [--with-tests] [--hook onRequest,onResponse]
|
|
6
|
+
*/
|
|
7
|
+
import { mkdir, writeFile } from 'fs/promises';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
const ALLOWED_HOOKS = [
|
|
12
|
+
'onConfigLoad', 'onComponentRegister', 'onRouteRegister',
|
|
13
|
+
'onStateInit', 'onServerStart', 'onRequest', 'onResponse',
|
|
14
|
+
'onError', 'onCliCommandRegister',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export async function createPlugin(pluginName, args = []) {
|
|
18
|
+
if (!pluginName) {
|
|
19
|
+
console.error('✗ Please provide a plugin name: africode create plugin my-plugin');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const isMinimal = args.includes('--minimal');
|
|
24
|
+
const withTests = args.includes('--with-tests') || !isMinimal;
|
|
25
|
+
const hookFlagIdx = args.indexOf('--hook');
|
|
26
|
+
const selectedHooks = hookFlagIdx > -1
|
|
27
|
+
? args[hookFlagIdx + 1]?.split(',').filter(h => ALLOWED_HOOKS.includes(h)) || ['onRequest', 'onResponse']
|
|
28
|
+
: ['onConfigLoad', 'onRequest', 'onResponse'];
|
|
29
|
+
|
|
30
|
+
const pluginDir = path.join(process.cwd(), pluginName);
|
|
31
|
+
|
|
32
|
+
if (existsSync(pluginDir)) {
|
|
33
|
+
console.error(`✗ Directory "${pluginName}" already exists`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(`\n🔌 Scaffolding AfriCode Plugin: ${pluginName}\n`);
|
|
38
|
+
|
|
39
|
+
// Create directory tree
|
|
40
|
+
await mkdir(path.join(pluginDir, 'src', 'hooks'), { recursive: true });
|
|
41
|
+
await mkdir(path.join(pluginDir, 'src', 'utils'), { recursive: true });
|
|
42
|
+
if (withTests) await mkdir(path.join(pluginDir, 'tests'), { recursive: true });
|
|
43
|
+
if (!isMinimal) await mkdir(path.join(pluginDir, 'examples'), { recursive: true });
|
|
44
|
+
|
|
45
|
+
// --- package.json ---
|
|
46
|
+
await writeFile(path.join(pluginDir, 'package.json'), JSON.stringify({
|
|
47
|
+
name: pluginName,
|
|
48
|
+
version: '1.0.0',
|
|
49
|
+
description: `AfriCode plugin: ${pluginName}`,
|
|
50
|
+
type: 'module',
|
|
51
|
+
main: 'index.js',
|
|
52
|
+
scripts: {
|
|
53
|
+
test: 'bun test',
|
|
54
|
+
},
|
|
55
|
+
keywords: ['africode', 'plugin', pluginName],
|
|
56
|
+
license: 'MIT',
|
|
57
|
+
peerDependencies: {
|
|
58
|
+
africode: '^3.0.0',
|
|
59
|
+
},
|
|
60
|
+
}, null, 2));
|
|
61
|
+
|
|
62
|
+
// --- manifest.js ---
|
|
63
|
+
await writeFile(path.join(pluginDir, 'src', 'manifest.js'),
|
|
64
|
+
`export default {
|
|
65
|
+
name: '${pluginName}',
|
|
66
|
+
version: '1.0.0',
|
|
67
|
+
compatibleWith: '^3.0.0',
|
|
68
|
+
hooks: ${JSON.stringify(selectedHooks)},
|
|
69
|
+
critical: false,
|
|
70
|
+
};
|
|
71
|
+
`);
|
|
72
|
+
|
|
73
|
+
// --- Hook files ---
|
|
74
|
+
for (const hook of selectedHooks) {
|
|
75
|
+
await writeFile(path.join(pluginDir, 'src', 'hooks', `${hook}.js`),
|
|
76
|
+
`/**
|
|
77
|
+
* ${hook} handler for ${pluginName}
|
|
78
|
+
*/
|
|
79
|
+
export default async function ${hook}(ctx) {
|
|
80
|
+
// Your plugin logic here
|
|
81
|
+
ctx.logger?.info?.(\`[${pluginName}] ${hook} executed\`);
|
|
82
|
+
}
|
|
83
|
+
`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Logger util ---
|
|
87
|
+
await writeFile(path.join(pluginDir, 'src', 'utils', 'logger.js'),
|
|
88
|
+
`/**
|
|
89
|
+
* Simple structured logger for ${pluginName}
|
|
90
|
+
*/
|
|
91
|
+
export const logger = {
|
|
92
|
+
info: (msg) => console.log(\`[INFO] \${msg}\`),
|
|
93
|
+
warn: (msg) => console.warn(\`[WARN] \${msg}\`),
|
|
94
|
+
error: (msg) => console.error(\`[ERROR] \${msg}\`),
|
|
95
|
+
};
|
|
96
|
+
`);
|
|
97
|
+
|
|
98
|
+
// --- index.js ---
|
|
99
|
+
const hookImports = selectedHooks.map(h => `import ${h} from './src/hooks/${h}.js';`).join('\n');
|
|
100
|
+
const hookEntries = selectedHooks.map(h => ` ${h},`).join('\n');
|
|
101
|
+
await writeFile(path.join(pluginDir, 'index.js'),
|
|
102
|
+
`import manifest from './src/manifest.js';
|
|
103
|
+
${hookImports}
|
|
104
|
+
|
|
105
|
+
export default {
|
|
106
|
+
manifest,
|
|
107
|
+
${hookEntries}
|
|
108
|
+
};
|
|
109
|
+
`);
|
|
110
|
+
|
|
111
|
+
// --- plugin.config.js ---
|
|
112
|
+
await writeFile(path.join(pluginDir, 'plugin.config.js'),
|
|
113
|
+
`/**
|
|
114
|
+
* Plugin configuration defaults.
|
|
115
|
+
* These can be overridden by the host application's config.
|
|
116
|
+
*/
|
|
117
|
+
export default {
|
|
118
|
+
enabled: true,
|
|
119
|
+
logLevel: 'info',
|
|
120
|
+
};
|
|
121
|
+
`);
|
|
122
|
+
|
|
123
|
+
// --- Tests ---
|
|
124
|
+
if (withTests) {
|
|
125
|
+
await writeFile(path.join(pluginDir, 'tests', 'plugin.test.js'),
|
|
126
|
+
`import { describe, it, expect } from 'bun:test';
|
|
127
|
+
import plugin from '../index.js';
|
|
128
|
+
|
|
129
|
+
describe('${pluginName}', () => {
|
|
130
|
+
it('should have a valid manifest', () => {
|
|
131
|
+
expect(plugin.manifest).toBeDefined();
|
|
132
|
+
expect(plugin.manifest.name).toBe('${pluginName}');
|
|
133
|
+
expect(plugin.manifest.version).toBeDefined();
|
|
134
|
+
expect(plugin.manifest.compatibleWith).toBeDefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should only declare allowed hooks', () => {
|
|
138
|
+
const allowed = new Set([
|
|
139
|
+
'onConfigLoad', 'onComponentRegister', 'onRouteRegister',
|
|
140
|
+
'onStateInit', 'onServerStart', 'onRequest', 'onResponse',
|
|
141
|
+
'onError', 'onCliCommandRegister',
|
|
142
|
+
]);
|
|
143
|
+
for (const hook of plugin.manifest.hooks) {
|
|
144
|
+
expect(allowed.has(hook)).toBe(true);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should expose all declared hooks as functions', () => {
|
|
149
|
+
for (const hook of plugin.manifest.hooks) {
|
|
150
|
+
expect(typeof plugin[hook]).toBe('function');
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('hooks should execute without throwing', async () => {
|
|
155
|
+
const mockCtx = { logger: { info: () => {}, warn: () => {}, error: () => {} } };
|
|
156
|
+
for (const hook of plugin.manifest.hooks) {
|
|
157
|
+
await expect(plugin[hook](mockCtx)).resolves.toBeUndefined();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- README.md ---
|
|
165
|
+
await writeFile(path.join(pluginDir, 'README.md'),
|
|
166
|
+
`# ${pluginName}
|
|
167
|
+
|
|
168
|
+
An AfriCode Framework plugin.
|
|
169
|
+
|
|
170
|
+
## What this plugin does
|
|
171
|
+
|
|
172
|
+
Describe your plugin's purpose here.
|
|
173
|
+
|
|
174
|
+
## Supported Hooks
|
|
175
|
+
|
|
176
|
+
${selectedHooks.map(h => `- \`${h}\``).join('\n')}
|
|
177
|
+
|
|
178
|
+
## Install
|
|
179
|
+
|
|
180
|
+
\`\`\`bash
|
|
181
|
+
bun add ${pluginName}
|
|
182
|
+
\`\`\`
|
|
183
|
+
|
|
184
|
+
## Register
|
|
185
|
+
|
|
186
|
+
\`\`\`javascript
|
|
187
|
+
import plugin from '${pluginName}';
|
|
188
|
+
|
|
189
|
+
africode.plugins.register(plugin.manifest, plugin);
|
|
190
|
+
\`\`\`
|
|
191
|
+
|
|
192
|
+
## Configuration
|
|
193
|
+
|
|
194
|
+
Edit \`plugin.config.js\` to adjust defaults.
|
|
195
|
+
|
|
196
|
+
## Troubleshooting
|
|
197
|
+
|
|
198
|
+
- Ensure your AfriCode version satisfies \`${'^3.0.0'}\`
|
|
199
|
+
- Check that only valid hooks are declared in the manifest
|
|
200
|
+
|
|
201
|
+
## Compatibility
|
|
202
|
+
|
|
203
|
+
| AfriCode Version | Plugin Version |
|
|
204
|
+
|:---:|:---:|
|
|
205
|
+
| ^3.0.0 | 1.0.0 |
|
|
206
|
+
`);
|
|
207
|
+
|
|
208
|
+
// --- Example usage ---
|
|
209
|
+
if (!isMinimal) {
|
|
210
|
+
await writeFile(path.join(pluginDir, 'examples', 'usage.js'),
|
|
211
|
+
`/**
|
|
212
|
+
* Example: registering ${pluginName} in an AfriCode app
|
|
213
|
+
*/
|
|
214
|
+
import { AfriPluginRegistry } from 'africode/core/plugins/index.js';
|
|
215
|
+
import plugin from '../index.js';
|
|
216
|
+
|
|
217
|
+
const registry = new AfriPluginRegistry();
|
|
218
|
+
registry.register(plugin.manifest, plugin);
|
|
219
|
+
|
|
220
|
+
// Simulate a hook emission
|
|
221
|
+
await registry.emit('onRequest', { req: {}, meta: {} });
|
|
222
|
+
console.log('Plugin executed successfully!');
|
|
223
|
+
`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log(`✓ Created ${pluginName}/`);
|
|
227
|
+
console.log(` ├── package.json`);
|
|
228
|
+
console.log(` ├── index.js`);
|
|
229
|
+
console.log(` ├── plugin.config.js`);
|
|
230
|
+
console.log(` ├── README.md`);
|
|
231
|
+
console.log(` ├── src/`);
|
|
232
|
+
console.log(` │ ├── manifest.js`);
|
|
233
|
+
console.log(` │ ├── hooks/`);
|
|
234
|
+
selectedHooks.forEach(h => console.log(` │ │ └── ${h}.js`));
|
|
235
|
+
console.log(` │ └── utils/logger.js`);
|
|
236
|
+
if (withTests) console.log(` ├── tests/plugin.test.js`);
|
|
237
|
+
if (!isMinimal) console.log(` └── examples/usage.js`);
|
|
238
|
+
console.log(`\n🚀 Next steps:`);
|
|
239
|
+
console.log(` cd ${pluginName}`);
|
|
240
|
+
console.log(` bun test\n`);
|
|
241
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { logo, colors } from '../ui.js';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
import { createHMRServer } from '../../hmr.js';
|
|
5
|
+
|
|
6
|
+
export async function dev() {
|
|
7
|
+
logo();
|
|
8
|
+
console.log(`${colors.green}▶ Starting development server...${colors.reset}\n`);
|
|
9
|
+
|
|
10
|
+
const PORT = process.env.PORT || 3000;
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = nodePath.dirname(__filename);
|
|
13
|
+
const frameworkRoot = nodePath.resolve(__dirname, '../../../');
|
|
14
|
+
|
|
15
|
+
// Initialize HMR server
|
|
16
|
+
const hmr = createHMRServer(PORT);
|
|
17
|
+
const clients = new Set();
|
|
18
|
+
|
|
19
|
+
const server = Bun.serve({
|
|
20
|
+
port: PORT,
|
|
21
|
+
idleTimeout: 30,
|
|
22
|
+
async fetch(req) {
|
|
23
|
+
const url = new URL(req.url);
|
|
24
|
+
let pathname = url.pathname;
|
|
25
|
+
|
|
26
|
+
// 1. API Route Delegation
|
|
27
|
+
if (pathname.startsWith('/api/')) {
|
|
28
|
+
const { handleApiRequest } = await import('../../server/router.js');
|
|
29
|
+
const apiRes = await handleApiRequest(req);
|
|
30
|
+
if (apiRes) {return apiRes;}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. Root Redirect
|
|
34
|
+
if (pathname === '/') {pathname = '/index';}
|
|
35
|
+
|
|
36
|
+
// 3. JS-First Page Rendering
|
|
37
|
+
const pageName = pathname.replace(/^\//, '').replace(/\.html$/, '');
|
|
38
|
+
const cwd = process.cwd();
|
|
39
|
+
const hasSrc = await Bun.file(nodePath.join(cwd, 'src/pages')).exists() || await Bun.file(nodePath.join(cwd, 'src')).exists();
|
|
40
|
+
const pagesDir = hasSrc ? 'src/pages' : 'pages';
|
|
41
|
+
const jsPagePath = `./${pagesDir}/${pageName}.js`;
|
|
42
|
+
|
|
43
|
+
if (await Bun.file(jsPagePath).exists()) {
|
|
44
|
+
const start = performance.now();
|
|
45
|
+
try {
|
|
46
|
+
const absPath = nodePath.resolve(cwd, pagesDir, `${pageName}.js`);
|
|
47
|
+
const moduleUrl = pathToFileURL(absPath).href + `?t=${Date.now()}`;
|
|
48
|
+
const pageModule = await import(moduleUrl);
|
|
49
|
+
let loaderData = {};
|
|
50
|
+
if (typeof pageModule.loader === 'function') {
|
|
51
|
+
loaderData = await pageModule.loader({ request: req, params: {} });
|
|
52
|
+
}
|
|
53
|
+
if (typeof pageModule.default === 'function') {
|
|
54
|
+
let htmlContent = pageModule.default({ data: loaderData });
|
|
55
|
+
// Inject HMR client
|
|
56
|
+
htmlContent = injectHMRClient(htmlContent);
|
|
57
|
+
const end = performance.now();
|
|
58
|
+
console.log(`${colors.blue} ⚡ Rendered ${pageName} in ${(end - start).toFixed(2)}ms${colors.reset}`);
|
|
59
|
+
return new Response(htmlContent, { headers: { 'Content-Type': 'text/html' } });
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error(`Error rendering page ${pageName}:`, err);
|
|
63
|
+
return new Response(`Error rendering page: ${err.message}`, { status: 500 });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 4. Fallback to Static HTML
|
|
68
|
+
if (await Bun.file(`./${pagesDir}${pathname}.html`).exists()) {
|
|
69
|
+
const { renderPage } = await import('../../server/render.js');
|
|
70
|
+
let response = await renderPage(`./${pagesDir}${pathname}.html`);
|
|
71
|
+
let htmlContent = await response.text();
|
|
72
|
+
// Inject HMR client
|
|
73
|
+
htmlContent = injectHMRClient(htmlContent);
|
|
74
|
+
return new Response(htmlContent, { headers: { 'Content-Type': 'text/html' } });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 5. Static Assets
|
|
78
|
+
const file = Bun.file('.' + url.pathname);
|
|
79
|
+
if (await file.exists()) {
|
|
80
|
+
if (pathname.endsWith('.html')) {
|
|
81
|
+
const { renderPage } = await import('../../server/render.js');
|
|
82
|
+
let response = await renderPage('.' + pathname);
|
|
83
|
+
let htmlContent = await response.text();
|
|
84
|
+
// Inject HMR client
|
|
85
|
+
htmlContent = injectHMRClient(htmlContent);
|
|
86
|
+
return new Response(htmlContent, { headers: { 'Content-Type': 'text/html' } });
|
|
87
|
+
}
|
|
88
|
+
const ext = pathname.split('.').pop();
|
|
89
|
+
const types = {
|
|
90
|
+
html: 'text/html', css: 'text/css', js: 'application/javascript',
|
|
91
|
+
json: 'application/json', svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg'
|
|
92
|
+
};
|
|
93
|
+
return new Response(file, { headers: { 'Content-Type': types[ext] || 'application/octet-stream' } });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 6. Framework Fallback (for linked projects)
|
|
97
|
+
const frameworkFile = Bun.file(nodePath.join(frameworkRoot, url.pathname));
|
|
98
|
+
if (await frameworkFile.exists()) {
|
|
99
|
+
const ext = pathname.split('.').pop();
|
|
100
|
+
const types = {
|
|
101
|
+
css: 'text/css', js: 'application/javascript',
|
|
102
|
+
svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg'
|
|
103
|
+
};
|
|
104
|
+
return new Response(frameworkFile, { headers: { 'Content-Type': types[ext] || 'application/octet-stream' } });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return new Response('404 Not Found', { status: 404 });
|
|
108
|
+
},
|
|
109
|
+
websocket: {
|
|
110
|
+
open(ws) {
|
|
111
|
+
console.log(`${colors.green}[HMR] Client connected${colors.reset}`);
|
|
112
|
+
clients.add(ws);
|
|
113
|
+
},
|
|
114
|
+
message(ws, message) {
|
|
115
|
+
try {
|
|
116
|
+
const data = JSON.parse(message);
|
|
117
|
+
if (data.type === 'ping') {
|
|
118
|
+
ws.send(JSON.stringify({ type: 'pong' }));
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// Invalid message, ignore
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
close(ws) {
|
|
125
|
+
clients.delete(ws);
|
|
126
|
+
console.log(`${colors.gold}[HMR] Client disconnected${colors.reset}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Helper function to inject HMR client into HTML
|
|
132
|
+
function injectHMRClient(htmlContent) {
|
|
133
|
+
if (typeof htmlContent !== 'string') {
|
|
134
|
+
return htmlContent;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const hmrScript = `<script>
|
|
138
|
+
(async function() {
|
|
139
|
+
let ws = null;
|
|
140
|
+
let reconnecting = false;
|
|
141
|
+
|
|
142
|
+
function connect() {
|
|
143
|
+
if (reconnecting) return;
|
|
144
|
+
if (ws && ws.readyState === WebSocket.OPEN) return;
|
|
145
|
+
|
|
146
|
+
ws = new WebSocket('ws://' + location.hostname + ':' + location.port);
|
|
147
|
+
ws.onopen = () => {
|
|
148
|
+
console.log('%c[HMR] Connected', 'color: green; font-weight: bold;');
|
|
149
|
+
reconnecting = false;
|
|
150
|
+
};
|
|
151
|
+
ws.onmessage = (e) => {
|
|
152
|
+
try {
|
|
153
|
+
const msg = JSON.parse(e.data);
|
|
154
|
+
if (msg.type === 'update') {
|
|
155
|
+
console.log('%c[HMR] Reloading...', 'color: #4CAF50; font-weight: bold;');
|
|
156
|
+
setTimeout(() => location.reload(), 100);
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {}
|
|
159
|
+
};
|
|
160
|
+
ws.onclose = () => {
|
|
161
|
+
console.log('%c[HMR] Disconnected', 'color: orange;');
|
|
162
|
+
if (!reconnecting) {
|
|
163
|
+
reconnecting = true;
|
|
164
|
+
setTimeout(connect, 1000);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
connect();
|
|
170
|
+
})();
|
|
171
|
+
</script>`;
|
|
172
|
+
|
|
173
|
+
// Inject before closing </body> tag if it exists, otherwise at the end
|
|
174
|
+
if (htmlContent.includes('</body>')) {
|
|
175
|
+
return htmlContent.replace('</body>', hmrScript + '</body>');
|
|
176
|
+
} else if (htmlContent.includes('</html>')) {
|
|
177
|
+
return htmlContent.replace('</html>', hmrScript + '</html>');
|
|
178
|
+
} else {
|
|
179
|
+
return htmlContent + hmrScript;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// File watching for HMR
|
|
184
|
+
async function watchFiles() {
|
|
185
|
+
const { watch } = await import('node:fs');
|
|
186
|
+
const ignoreDirs = new Set(['node_modules', '.git', 'dist', '.bun', 'build']);
|
|
187
|
+
|
|
188
|
+
function watchDir(dir) {
|
|
189
|
+
try {
|
|
190
|
+
const watcher = watch(dir, { recursive: true }, (eventType, filename) => {
|
|
191
|
+
if (!filename) return;
|
|
192
|
+
|
|
193
|
+
const dirName = filename.split(nodePath.sep)[0];
|
|
194
|
+
if (ignoreDirs.has(dirName) || filename.includes('node_modules')) return;
|
|
195
|
+
|
|
196
|
+
const validExts = ['.js', '.jsx', '.ts', '.tsx', '.css', '.html'];
|
|
197
|
+
if (!validExts.some(ext => filename.endsWith(ext))) return;
|
|
198
|
+
|
|
199
|
+
console.log(`${colors.blue}[HMR] File changed: ${filename}${colors.reset}`);
|
|
200
|
+
|
|
201
|
+
// Broadcast to all clients
|
|
202
|
+
const message = JSON.stringify({
|
|
203
|
+
type: 'update',
|
|
204
|
+
file: filename,
|
|
205
|
+
timestamp: Date.now()
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
for (const client of clients) {
|
|
209
|
+
try {
|
|
210
|
+
client.send(message);
|
|
211
|
+
} catch (err) {}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.error('[HMR] Watch error:', err);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
watchDir(process.cwd());
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Start file watching
|
|
223
|
+
watchFiles();
|
|
224
|
+
|
|
225
|
+
console.log(`${colors.green}✓ Server running at ${colors.blue}http://localhost:${server.port}${colors.reset}`);
|
|
226
|
+
console.log(`${colors.green}✓ HMR enabled - Changes will reload automatically${colors.reset}`);
|
|
227
|
+
console.log(`${colors.gold} Press Ctrl+C to stop${colors.reset}\n`);
|
|
228
|
+
}
|