create-nativecore 0.1.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/README.md +38 -0
- package/bin/index.mjs +717 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# create-nativecore
|
|
2
|
+
|
|
3
|
+
Official CLI for generating NativeCore applications.
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
|
|
7
|
+
- prompt for common project setup decisions
|
|
8
|
+
- generate app-level shells, routes, controllers, views, and styles
|
|
9
|
+
- keep framework internals inside the published `nativecorejs` package
|
|
10
|
+
- avoid shipping demo API code into new projects by default
|
|
11
|
+
|
|
12
|
+
## Current starter behavior
|
|
13
|
+
|
|
14
|
+
- installs `nativecorejs` as the framework dependency
|
|
15
|
+
- runs the selected package manager automatically after scaffolding unless `--skip-install` is passed
|
|
16
|
+
- uses an import map so browser-loaded ESM can resolve `nativecorejs` without a bundler
|
|
17
|
+
- links the framework base stylesheet from `node_modules/nativecorejs/src/styles/base.css`
|
|
18
|
+
- registers built-in framework components during app bootstrap
|
|
19
|
+
|
|
20
|
+
## Local workspace mode
|
|
21
|
+
|
|
22
|
+
- pass `--local` to generate `"nativecorejs": "file:../packages/nativecorejs"`
|
|
23
|
+
- this is intended for working inside the `nativecorejs` monorepo before publishing to npm
|
|
24
|
+
- generated local starters still expect the framework package to be built so `dist/` exists
|
|
25
|
+
|
|
26
|
+
## Install behavior
|
|
27
|
+
|
|
28
|
+
- `npx create-nativecore my-app` should scaffold the project and run `npm install` immediately by default
|
|
29
|
+
- use `--pm=pnpm` or `--pm=yarn` to install with a different package manager
|
|
30
|
+
- use `--skip-install` when you only want files generated without dependency installation
|
|
31
|
+
|
|
32
|
+
## Planned templates
|
|
33
|
+
|
|
34
|
+
- landing plus app shell
|
|
35
|
+
- docs starter
|
|
36
|
+
- auth starter
|
|
37
|
+
- dashboard starter
|
|
38
|
+
- future deployment targets such as Cloudflare or Node
|
package/bin/index.mjs
ADDED
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { createInterface } from 'readline/promises';
|
|
7
|
+
import { stdin as input, stdout as output } from 'process';
|
|
8
|
+
|
|
9
|
+
const cliArgs = process.argv.slice(2);
|
|
10
|
+
const rl = createInterface({ input, output });
|
|
11
|
+
|
|
12
|
+
function hasFlag(flag) {
|
|
13
|
+
return cliArgs.includes(flag);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getFlagValue(prefix) {
|
|
17
|
+
const match = cliArgs.find(arg => arg.startsWith(`${prefix}=`));
|
|
18
|
+
return match ? match.slice(prefix.length + 1) : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toKebabCase(value) {
|
|
22
|
+
return value
|
|
23
|
+
.trim()
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
26
|
+
.replace(/\s+/g, '-')
|
|
27
|
+
.replace(/-+/g, '-');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toTitleCase(value) {
|
|
31
|
+
return value
|
|
32
|
+
.split(/[-\s]/)
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
35
|
+
.join(' ');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function ask(question, fallback = '') {
|
|
39
|
+
const suffix = fallback ? ` (${fallback})` : '';
|
|
40
|
+
const answer = await rl.question(`${question}${suffix}: `);
|
|
41
|
+
return answer.trim() || fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function askYesNo(question, defaultYes = true) {
|
|
45
|
+
const fallback = defaultYes ? 'y' : 'n';
|
|
46
|
+
const answer = (await ask(question, fallback)).toLowerCase();
|
|
47
|
+
return answer === 'y' || answer === 'yes';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function askChoice(question, options, fallback) {
|
|
51
|
+
const label = `${question} [${options.join('/')}]`;
|
|
52
|
+
while (true) {
|
|
53
|
+
const answer = (await ask(label, fallback)).toLowerCase();
|
|
54
|
+
if (options.includes(answer)) return answer;
|
|
55
|
+
console.log(`Invalid choice. Expected one of: ${options.join(', ')}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function ensureDir(dirPath) {
|
|
60
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function writeFile(filePath, content) {
|
|
64
|
+
await ensureDir(path.dirname(filePath));
|
|
65
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function installCommand(packageManager) {
|
|
69
|
+
if (packageManager === 'yarn') {
|
|
70
|
+
return { command: 'yarn', args: ['install'] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { command: packageManager, args: ['install'] };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function installDependencies(targetDir, packageManager) {
|
|
77
|
+
const { command, args } = installCommand(packageManager);
|
|
78
|
+
|
|
79
|
+
await new Promise((resolve, reject) => {
|
|
80
|
+
const child = spawn(command, args, {
|
|
81
|
+
cwd: targetDir,
|
|
82
|
+
stdio: 'inherit',
|
|
83
|
+
shell: process.platform === 'win32'
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
child.on('error', reject);
|
|
87
|
+
child.on('exit', code => {
|
|
88
|
+
if (code === 0) {
|
|
89
|
+
resolve();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
reject(new Error(`${packageManager} install failed with exit code ${code ?? 'unknown'}`));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function scriptBlock(config) {
|
|
99
|
+
const scripts = {
|
|
100
|
+
dev: `${config.packageManager === 'yarn' ? 'yarn compile' : `${config.packageManager} run compile`} && node server.js`,
|
|
101
|
+
start: 'node server.js',
|
|
102
|
+
compile: config.useTypeScript ? 'tsc' : 'echo "No compile step required for JavaScript"',
|
|
103
|
+
typecheck: config.useTypeScript ? 'tsc --noEmit' : 'echo "Type checking is disabled for JavaScript mode"'
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return scripts;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function packageJsonTemplate(config) {
|
|
110
|
+
return JSON.stringify({
|
|
111
|
+
name: config.projectName,
|
|
112
|
+
version: '0.1.0',
|
|
113
|
+
private: true,
|
|
114
|
+
type: 'module',
|
|
115
|
+
scripts: scriptBlock(config),
|
|
116
|
+
dependencies: {
|
|
117
|
+
nativecorejs: config.frameworkDependency
|
|
118
|
+
},
|
|
119
|
+
devDependencies: config.useTypeScript ? {
|
|
120
|
+
typescript: '^5.6.3',
|
|
121
|
+
'@types/node': '^22.0.0'
|
|
122
|
+
} : {}
|
|
123
|
+
}, null, 2) + '\n';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function tsconfigTemplate() {
|
|
127
|
+
return `{
|
|
128
|
+
"compilerOptions": {
|
|
129
|
+
"target": "ES2022",
|
|
130
|
+
"module": "ESNext",
|
|
131
|
+
"moduleResolution": "Bundler",
|
|
132
|
+
"outDir": "dist",
|
|
133
|
+
"strict": true,
|
|
134
|
+
"skipLibCheck": true,
|
|
135
|
+
"baseUrl": "."
|
|
136
|
+
},
|
|
137
|
+
"include": ["src/**/*.ts"]
|
|
138
|
+
}
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function serverTemplate() {
|
|
143
|
+
return `import http from 'http';
|
|
144
|
+
import fs from 'fs';
|
|
145
|
+
import path from 'path';
|
|
146
|
+
import { fileURLToPath } from 'url';
|
|
147
|
+
|
|
148
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
149
|
+
const __dirname = path.dirname(__filename);
|
|
150
|
+
const PORT = process.env.PORT || 8000;
|
|
151
|
+
|
|
152
|
+
const mimeTypes = {
|
|
153
|
+
'.html': 'text/html',
|
|
154
|
+
'.js': 'text/javascript',
|
|
155
|
+
'.css': 'text/css',
|
|
156
|
+
'.json': 'application/json',
|
|
157
|
+
'.svg': 'image/svg+xml'
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
http.createServer((req, res) => {
|
|
161
|
+
const requestPath = req.url?.split('?')[0] || '/';
|
|
162
|
+
let filePath = path.join(__dirname, requestPath === '/' ? 'index.html' : requestPath);
|
|
163
|
+
|
|
164
|
+
if (!path.extname(requestPath) && !fs.existsSync(filePath)) {
|
|
165
|
+
filePath = path.join(__dirname, 'index.html');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fs.readFile(filePath, (error, content) => {
|
|
169
|
+
if (error) {
|
|
170
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
171
|
+
res.end('Not found');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const contentType = mimeTypes[path.extname(filePath)] || 'text/plain';
|
|
176
|
+
res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'no-cache' });
|
|
177
|
+
res.end(content);
|
|
178
|
+
});
|
|
179
|
+
}).listen(PORT, () => {
|
|
180
|
+
console.log('NativeCore starter running at http://localhost:' + PORT);
|
|
181
|
+
});
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function shellHtmlTemplate(config, shell) {
|
|
186
|
+
const entryScript = config.useTypeScript ? './dist/app.js' : './src/app.js';
|
|
187
|
+
const shellName = shell === 'app' ? 'protected' : 'public';
|
|
188
|
+
|
|
189
|
+
return `<!DOCTYPE html>
|
|
190
|
+
<html lang="en">
|
|
191
|
+
<head>
|
|
192
|
+
<meta charset="UTF-8">
|
|
193
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
194
|
+
<meta name="app-shell" content="${shellName}">
|
|
195
|
+
<title>${config.projectTitle}</title>
|
|
196
|
+
<link rel="stylesheet" href="./node_modules/nativecorejs/src/styles/base.css">
|
|
197
|
+
<link rel="stylesheet" href="./src/styles/main.css">
|
|
198
|
+
<script type="importmap">
|
|
199
|
+
{
|
|
200
|
+
"imports": {
|
|
201
|
+
"nativecorejs": "./node_modules/nativecorejs/dist/index.js",
|
|
202
|
+
"nativecorejs/components": "./node_modules/nativecorejs/dist/components/index.js"
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
</script>
|
|
206
|
+
</head>
|
|
207
|
+
<body>
|
|
208
|
+
<div id="main-content"></div>
|
|
209
|
+
<script type="module" src="${entryScript}"></script>
|
|
210
|
+
</body>
|
|
211
|
+
</html>
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function appEntryTemplate(config) {
|
|
216
|
+
return `import { registerBuiltinComponents, Router } from 'nativecorejs';
|
|
217
|
+
import { registerRoutes } from './config/routes.js';
|
|
218
|
+
|
|
219
|
+
registerBuiltinComponents();
|
|
220
|
+
|
|
221
|
+
const router = new Router();
|
|
222
|
+
|
|
223
|
+
registerRoutes(router);
|
|
224
|
+
router.start();
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function routesTemplate(config) {
|
|
229
|
+
const typeImport = config.useTypeScript
|
|
230
|
+
? "import type { ControllerFunction, Router } from 'nativecorejs';\n"
|
|
231
|
+
: '';
|
|
232
|
+
|
|
233
|
+
const docsRoute = config.includeDocs
|
|
234
|
+
? " .register('/docs', 'src/views/pages/public/docs.html', lazyController('docsController', '../controllers/docs.controller.js'))\n"
|
|
235
|
+
: '';
|
|
236
|
+
const loginRoute = config.includeAuth
|
|
237
|
+
? " .register('/login', 'src/views/pages/public/login.html', lazyController('loginController', '../controllers/login.controller.js'))\n"
|
|
238
|
+
: '';
|
|
239
|
+
const dashboardRoute = config.includeDashboard
|
|
240
|
+
? " .register('/dashboard', 'src/views/pages/protected/dashboard.html', lazyController('dashboardController', '../controllers/dashboard.controller.js'))\n"
|
|
241
|
+
: '';
|
|
242
|
+
const protectedRoutes = config.includeDashboard ? "export const protectedRoutes = ['/dashboard'];\n" : "export const protectedRoutes = [];\n";
|
|
243
|
+
|
|
244
|
+
return `${typeImport}function lazyController(controllerName${config.useTypeScript ? ': string' : ''}, controllerPath${config.useTypeScript ? ': string' : ''})${config.useTypeScript ? ': ControllerFunction' : ''} {
|
|
245
|
+
return async (...args) => {
|
|
246
|
+
const module = await import(controllerPath);
|
|
247
|
+
return module[controllerName](...args);
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function registerRoutes(router${config.useTypeScript ? ': Router' : ''})${config.useTypeScript ? ': void' : ''} {
|
|
252
|
+
router
|
|
253
|
+
.register('/', 'src/views/pages/public/home.html', lazyController('homeController', '../controllers/home.controller.js'))
|
|
254
|
+
${docsRoute}${loginRoute}${dashboardRoute}}
|
|
255
|
+
|
|
256
|
+
${protectedRoutes}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function controllerTemplate(name, body, config) {
|
|
260
|
+
const typePrefix = config.useTypeScript ? ': Promise<() => void>' : '';
|
|
261
|
+
return `import { trackEvents, trackSubscriptions } from 'nativecorejs';
|
|
262
|
+
|
|
263
|
+
export async function ${name}(params = {})${typePrefix} {
|
|
264
|
+
const events = trackEvents();
|
|
265
|
+
const subs = trackSubscriptions();
|
|
266
|
+
|
|
267
|
+
${body}
|
|
268
|
+
|
|
269
|
+
return () => {
|
|
270
|
+
events.cleanup();
|
|
271
|
+
subs.cleanup();
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function homeControllerBody(config) {
|
|
278
|
+
return ` void params;
|
|
279
|
+
const cta = document.querySelector('[data-action="launch-dashboard"]');
|
|
280
|
+
|
|
281
|
+
if (cta) {
|
|
282
|
+
events.onClick('[data-action="launch-dashboard"]', () => {
|
|
283
|
+
window.history.pushState({}, '', '${config.includeDashboard ? '/dashboard' : '/'}');
|
|
284
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
285
|
+
});
|
|
286
|
+
}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function loginControllerBody() {
|
|
290
|
+
return ` void params;
|
|
291
|
+
events.onSubmit('[data-form="login"]', event => {
|
|
292
|
+
event.preventDefault();
|
|
293
|
+
});`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function docsControllerBody() {
|
|
297
|
+
return ' void params;';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function dashboardControllerBody() {
|
|
301
|
+
return ` void params;
|
|
302
|
+
const items = document.querySelectorAll('[data-metric-card]');
|
|
303
|
+
items.forEach(item => item.classList.add('is-ready'));`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function publicViewTemplate(config) {
|
|
307
|
+
const docsLink = config.includeDocs ? '<a href="/docs">Docs</a>' : '';
|
|
308
|
+
const authLink = config.includeAuth ? '<a href="/login">Login</a>' : '';
|
|
309
|
+
const dashboardButton = config.includeDashboard ? '<button type="button" data-action="launch-dashboard">Open dashboard shell</button>' : '';
|
|
310
|
+
|
|
311
|
+
return `<section class="hero">
|
|
312
|
+
<p class="eyebrow">NativeCore</p>
|
|
313
|
+
<h1>${config.projectTitle}</h1>
|
|
314
|
+
<p class="lede">A clean starter generated by create-nativecore. This shell is app-level only and excludes demo API endpoints or deployment-specific backend assets.</p>
|
|
315
|
+
<div class="hero-actions">
|
|
316
|
+
${dashboardButton}
|
|
317
|
+
${docsLink}
|
|
318
|
+
${authLink}
|
|
319
|
+
</div>
|
|
320
|
+
</section>
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function docsViewTemplate() {
|
|
325
|
+
return `<section class="page-section">
|
|
326
|
+
<h1>Documentation</h1>
|
|
327
|
+
<p>Replace this starter page with your product documentation, component examples, or a markdown renderer.</p>
|
|
328
|
+
</section>
|
|
329
|
+
`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function loginViewTemplate() {
|
|
333
|
+
return `<section class="page-section auth-page">
|
|
334
|
+
<h1>Sign in</h1>
|
|
335
|
+
<form data-form="login" class="auth-form">
|
|
336
|
+
<label>
|
|
337
|
+
<span>Email</span>
|
|
338
|
+
<input type="email" name="email" placeholder="you@example.com">
|
|
339
|
+
</label>
|
|
340
|
+
<label>
|
|
341
|
+
<span>Password</span>
|
|
342
|
+
<input type="password" name="password" placeholder="Enter your password">
|
|
343
|
+
</label>
|
|
344
|
+
<button type="submit">Sign in</button>
|
|
345
|
+
</form>
|
|
346
|
+
</section>
|
|
347
|
+
`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function dashboardViewTemplate() {
|
|
351
|
+
return `<section class="page-section dashboard-grid">
|
|
352
|
+
<article data-metric-card>
|
|
353
|
+
<h2>Users</h2>
|
|
354
|
+
<p>1,284</p>
|
|
355
|
+
</article>
|
|
356
|
+
<article data-metric-card>
|
|
357
|
+
<h2>Revenue</h2>
|
|
358
|
+
<p>$48,900</p>
|
|
359
|
+
</article>
|
|
360
|
+
<article data-metric-card>
|
|
361
|
+
<h2>Errors</h2>
|
|
362
|
+
<p>2</p>
|
|
363
|
+
</article>
|
|
364
|
+
</section>
|
|
365
|
+
`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function stylesTemplate() {
|
|
369
|
+
return `:root {
|
|
370
|
+
--background: #f5efe5;
|
|
371
|
+
--surface: rgba(255, 255, 255, 0.78);
|
|
372
|
+
--surface-strong: #fffaf2;
|
|
373
|
+
--text: #1f2937;
|
|
374
|
+
--muted: #5b6470;
|
|
375
|
+
--accent: #0f766e;
|
|
376
|
+
--accent-strong: #115e59;
|
|
377
|
+
--border: rgba(31, 41, 55, 0.12);
|
|
378
|
+
--shadow: 0 24px 60px rgba(15, 23, 42, 0.10);
|
|
379
|
+
font-family: 'Space Grotesk', 'Segoe UI', sans-serif;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
* {
|
|
383
|
+
box-sizing: border-box;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
body {
|
|
387
|
+
margin: 0;
|
|
388
|
+
min-height: 100vh;
|
|
389
|
+
color: var(--text);
|
|
390
|
+
background:
|
|
391
|
+
radial-gradient(circle at top left, rgba(15, 118, 110, 0.18), transparent 32%),
|
|
392
|
+
radial-gradient(circle at bottom right, rgba(217, 119, 6, 0.16), transparent 24%),
|
|
393
|
+
linear-gradient(180deg, #f8f5ee 0%, #efe6d8 100%);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
body::before {
|
|
397
|
+
content: '';
|
|
398
|
+
position: fixed;
|
|
399
|
+
inset: 0;
|
|
400
|
+
background-image: linear-gradient(rgba(255, 255, 255, 0.18) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.18) 1px, transparent 1px);
|
|
401
|
+
background-size: 28px 28px;
|
|
402
|
+
pointer-events: none;
|
|
403
|
+
opacity: 0.5;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#main-content {
|
|
407
|
+
position: relative;
|
|
408
|
+
z-index: 1;
|
|
409
|
+
width: min(1100px, calc(100vw - 2rem));
|
|
410
|
+
margin: 0 auto;
|
|
411
|
+
padding: 4rem 0 5rem;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.hero,
|
|
415
|
+
.page-section {
|
|
416
|
+
background: var(--surface);
|
|
417
|
+
backdrop-filter: blur(14px);
|
|
418
|
+
border: 1px solid var(--border);
|
|
419
|
+
border-radius: 28px;
|
|
420
|
+
box-shadow: var(--shadow);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.hero {
|
|
424
|
+
padding: 4rem;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.eyebrow {
|
|
428
|
+
margin: 0 0 1rem;
|
|
429
|
+
text-transform: uppercase;
|
|
430
|
+
letter-spacing: 0.18em;
|
|
431
|
+
color: var(--accent-strong);
|
|
432
|
+
font-size: 0.82rem;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
h1,
|
|
436
|
+
h2,
|
|
437
|
+
p {
|
|
438
|
+
margin-top: 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
h1 {
|
|
442
|
+
font-size: clamp(2.5rem, 6vw, 5rem);
|
|
443
|
+
line-height: 0.95;
|
|
444
|
+
margin-bottom: 1rem;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.lede,
|
|
448
|
+
.page-section p {
|
|
449
|
+
max-width: 42rem;
|
|
450
|
+
font-size: 1.05rem;
|
|
451
|
+
line-height: 1.7;
|
|
452
|
+
color: var(--muted);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.hero-actions {
|
|
456
|
+
display: flex;
|
|
457
|
+
flex-wrap: wrap;
|
|
458
|
+
gap: 0.9rem;
|
|
459
|
+
margin-top: 2rem;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
button,
|
|
463
|
+
a {
|
|
464
|
+
display: inline-flex;
|
|
465
|
+
align-items: center;
|
|
466
|
+
justify-content: center;
|
|
467
|
+
min-height: 2.9rem;
|
|
468
|
+
padding: 0.75rem 1.1rem;
|
|
469
|
+
border-radius: 999px;
|
|
470
|
+
border: 1px solid transparent;
|
|
471
|
+
text-decoration: none;
|
|
472
|
+
font: inherit;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
button {
|
|
476
|
+
background: var(--accent);
|
|
477
|
+
color: #fff;
|
|
478
|
+
cursor: pointer;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
button:hover {
|
|
482
|
+
background: var(--accent-strong);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
a {
|
|
486
|
+
color: var(--text);
|
|
487
|
+
background: rgba(255, 255, 255, 0.65);
|
|
488
|
+
border-color: var(--border);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.page-section {
|
|
492
|
+
padding: 2rem;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.auth-page {
|
|
496
|
+
max-width: 34rem;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.auth-form {
|
|
500
|
+
display: grid;
|
|
501
|
+
gap: 1rem;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.auth-form label {
|
|
505
|
+
display: grid;
|
|
506
|
+
gap: 0.45rem;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.auth-form input {
|
|
510
|
+
min-height: 3rem;
|
|
511
|
+
border: 1px solid var(--border);
|
|
512
|
+
border-radius: 16px;
|
|
513
|
+
padding: 0.8rem 0.95rem;
|
|
514
|
+
font: inherit;
|
|
515
|
+
background: rgba(255, 255, 255, 0.85);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.dashboard-grid {
|
|
519
|
+
display: grid;
|
|
520
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
521
|
+
gap: 1rem;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.dashboard-grid article {
|
|
525
|
+
padding: 1.5rem;
|
|
526
|
+
border-radius: 20px;
|
|
527
|
+
background: var(--surface-strong);
|
|
528
|
+
border: 1px solid var(--border);
|
|
529
|
+
transform: translateY(8px);
|
|
530
|
+
opacity: 0;
|
|
531
|
+
transition: transform 160ms ease, opacity 160ms ease;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.dashboard-grid article.is-ready {
|
|
535
|
+
transform: translateY(0);
|
|
536
|
+
opacity: 1;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
@media (max-width: 720px) {
|
|
540
|
+
#main-content {
|
|
541
|
+
width: min(100vw - 1rem, 100%);
|
|
542
|
+
padding-top: 1rem;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.hero,
|
|
546
|
+
.page-section {
|
|
547
|
+
border-radius: 22px;
|
|
548
|
+
padding: 1.4rem;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
`;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function nativecoreConfigTemplate(config) {
|
|
555
|
+
return JSON.stringify({
|
|
556
|
+
appName: config.projectTitle,
|
|
557
|
+
packageManager: config.packageManager,
|
|
558
|
+
useTypeScript: config.useTypeScript,
|
|
559
|
+
frameworkDependency: config.frameworkDependency,
|
|
560
|
+
features: {
|
|
561
|
+
authShell: config.includeAuth,
|
|
562
|
+
docs: config.includeDocs,
|
|
563
|
+
dashboard: config.includeDashboard
|
|
564
|
+
}
|
|
565
|
+
}, null, 2) + '\n';
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function buildProject(config) {
|
|
569
|
+
const targetDir = path.resolve(process.cwd(), config.projectName);
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
await fs.access(targetDir);
|
|
573
|
+
throw new Error(`Target directory already exists: ${config.projectName}`);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (error.code !== 'ENOENT') throw error;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const sourceExtension = config.useTypeScript ? 'ts' : 'js';
|
|
579
|
+
|
|
580
|
+
await ensureDir(targetDir);
|
|
581
|
+
await ensureDir(path.join(targetDir, 'src/config'));
|
|
582
|
+
await ensureDir(path.join(targetDir, 'src/controllers'));
|
|
583
|
+
await ensureDir(path.join(targetDir, 'src/views/pages/public'));
|
|
584
|
+
await ensureDir(path.join(targetDir, 'src/views/pages/protected'));
|
|
585
|
+
await ensureDir(path.join(targetDir, 'src/styles'));
|
|
586
|
+
|
|
587
|
+
await writeFile(path.join(targetDir, 'package.json'), packageJsonTemplate(config));
|
|
588
|
+
await writeFile(path.join(targetDir, 'server.js'), serverTemplate());
|
|
589
|
+
await writeFile(path.join(targetDir, 'index.html'), shellHtmlTemplate(config, 'index'));
|
|
590
|
+
await writeFile(path.join(targetDir, 'nativecore.config.json'), nativecoreConfigTemplate(config));
|
|
591
|
+
await writeFile(path.join(targetDir, `src/app.${sourceExtension}`), appEntryTemplate(config));
|
|
592
|
+
await writeFile(path.join(targetDir, `src/config/routes.${sourceExtension}`), routesTemplate(config));
|
|
593
|
+
await writeFile(path.join(targetDir, `src/controllers/home.controller.${sourceExtension}`), controllerTemplate('homeController', homeControllerBody(config), config));
|
|
594
|
+
await writeFile(path.join(targetDir, 'src/views/pages/public/home.html'), publicViewTemplate(config));
|
|
595
|
+
await writeFile(path.join(targetDir, 'src/styles/main.css'), stylesTemplate());
|
|
596
|
+
|
|
597
|
+
if (config.useTypeScript) {
|
|
598
|
+
await writeFile(path.join(targetDir, 'tsconfig.json'), tsconfigTemplate());
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (config.includeAuth) {
|
|
602
|
+
await writeFile(path.join(targetDir, 'app.html'), shellHtmlTemplate(config, 'app'));
|
|
603
|
+
await writeFile(path.join(targetDir, `src/controllers/login.controller.${sourceExtension}`), controllerTemplate('loginController', loginControllerBody(), config));
|
|
604
|
+
await writeFile(path.join(targetDir, 'src/views/pages/public/login.html'), loginViewTemplate());
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (config.includeDocs) {
|
|
608
|
+
await writeFile(path.join(targetDir, `src/controllers/docs.controller.${sourceExtension}`), controllerTemplate('docsController', docsControllerBody(), config));
|
|
609
|
+
await writeFile(path.join(targetDir, 'src/views/pages/public/docs.html'), docsViewTemplate());
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (config.includeDashboard) {
|
|
613
|
+
await writeFile(path.join(targetDir, `src/controllers/dashboard.controller.${sourceExtension}`), controllerTemplate('dashboardController', dashboardControllerBody(), config));
|
|
614
|
+
await writeFile(path.join(targetDir, 'src/views/pages/protected/dashboard.html'), dashboardViewTemplate());
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return targetDir;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function main() {
|
|
621
|
+
console.log('\nNativeCore installer\n');
|
|
622
|
+
|
|
623
|
+
const positionalName = cliArgs.find(arg => !arg.startsWith('--'));
|
|
624
|
+
const rawName = positionalName || await ask('Project name', 'my-nativecore-app');
|
|
625
|
+
const projectName = toKebabCase(rawName);
|
|
626
|
+
const projectTitle = toTitleCase(projectName);
|
|
627
|
+
const useDefaults = hasFlag('--defaults');
|
|
628
|
+
|
|
629
|
+
const useTypeScript = hasFlag('--js')
|
|
630
|
+
? false
|
|
631
|
+
: hasFlag('--ts')
|
|
632
|
+
? true
|
|
633
|
+
: useDefaults
|
|
634
|
+
? true
|
|
635
|
+
: await askYesNo('Use TypeScript?', true);
|
|
636
|
+
const useLocalFramework = hasFlag('--local')
|
|
637
|
+
? true
|
|
638
|
+
: useDefaults
|
|
639
|
+
? false
|
|
640
|
+
: await askYesNo('Use local workspace nativecorejs package?', false);
|
|
641
|
+
const includeAuth = hasFlag('--no-auth')
|
|
642
|
+
? false
|
|
643
|
+
: useDefaults
|
|
644
|
+
? true
|
|
645
|
+
: await askYesNo('Include auth shell?', true);
|
|
646
|
+
const includeDocs = hasFlag('--no-docs')
|
|
647
|
+
? false
|
|
648
|
+
: useDefaults
|
|
649
|
+
? true
|
|
650
|
+
: await askYesNo('Include docs route?', true);
|
|
651
|
+
const includeDashboard = hasFlag('--no-dashboard')
|
|
652
|
+
? false
|
|
653
|
+
: useDefaults
|
|
654
|
+
? true
|
|
655
|
+
: await askYesNo('Include dashboard route?', true);
|
|
656
|
+
const packageManager = getFlagValue('--pm')
|
|
657
|
+
|| (useDefaults ? 'npm' : await askChoice('Package manager', ['npm', 'pnpm', 'yarn'], 'npm'));
|
|
658
|
+
const shouldInstall = hasFlag('--skip-install') || hasFlag('--no-install')
|
|
659
|
+
? false
|
|
660
|
+
: useDefaults
|
|
661
|
+
? true
|
|
662
|
+
: await askYesNo('Install dependencies now?', true);
|
|
663
|
+
|
|
664
|
+
const config = {
|
|
665
|
+
projectName,
|
|
666
|
+
projectTitle,
|
|
667
|
+
useTypeScript,
|
|
668
|
+
frameworkDependency: useLocalFramework ? 'file:../packages/nativecorejs' : '^0.1.0',
|
|
669
|
+
includeAuth,
|
|
670
|
+
includeDocs,
|
|
671
|
+
includeDashboard,
|
|
672
|
+
packageManager,
|
|
673
|
+
shouldInstall
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
const targetDir = await buildProject(config);
|
|
677
|
+
|
|
678
|
+
let installSucceeded = false;
|
|
679
|
+
let installError = null;
|
|
680
|
+
|
|
681
|
+
if (config.shouldInstall) {
|
|
682
|
+
console.log(`\nInstalling dependencies with ${config.packageManager}...\n`);
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
await installDependencies(targetDir, config.packageManager);
|
|
686
|
+
installSucceeded = true;
|
|
687
|
+
} catch (error) {
|
|
688
|
+
installError = error;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
console.log('\nProject created successfully.');
|
|
693
|
+
console.log(`cd ${config.projectName}`);
|
|
694
|
+
|
|
695
|
+
if (config.shouldInstall && installSucceeded) {
|
|
696
|
+
console.log('Dependencies installed.');
|
|
697
|
+
} else {
|
|
698
|
+
console.log(`${config.packageManager} install`);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (installError) {
|
|
702
|
+
console.log('\nDependency installation did not complete.');
|
|
703
|
+
console.log(installError.message);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
console.log(`${config.packageManager} run dev`);
|
|
707
|
+
console.log('\nThis starter expects nativecorejs to provide prebuilt dist files and the base stylesheet from node_modules/nativecorejs.');
|
|
708
|
+
|
|
709
|
+
rl.close();
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
main().catch(error => {
|
|
713
|
+
console.error('\nFailed to scaffold NativeCore app.');
|
|
714
|
+
console.error(error.message);
|
|
715
|
+
rl.close();
|
|
716
|
+
process.exit(1);
|
|
717
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-nativecore",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Interactive CLI for scaffolding NativeCore applications",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nativecore",
|
|
7
|
+
"cli",
|
|
8
|
+
"scaffold",
|
|
9
|
+
"starter",
|
|
10
|
+
"web-components"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/davidtv2008/nativecorejs#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/davidtv2008/nativecorejs/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/davidtv2008/nativecorejs.git"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"bin": {
|
|
22
|
+
"create-nativecore": "./bin/index.mjs"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "echo create-nativecore is a direct CLI package"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|