tova 0.8.2 → 0.9.4
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/bin/tova.js +1128 -137
- package/package.json +14 -2
- package/src/analyzer/analyzer.js +405 -9
- package/src/analyzer/browser-analyzer.js +56 -8
- package/src/analyzer/scope.js +7 -0
- package/src/analyzer/server-analyzer.js +33 -1
- package/src/codegen/base-codegen.js +137 -13
- package/src/codegen/browser-codegen.js +725 -20
- package/src/codegen/codegen.js +67 -5
- package/src/codegen/server-codegen.js +54 -6
- package/src/codegen/theme-codegen.js +69 -0
- package/src/config/module-path.js +34 -2
- package/src/config/resolve.js +9 -0
- package/src/config/toml.js +13 -1
- package/src/deploy/provision.js +6 -2
- package/src/diagnostics/security-scorecard.js +111 -0
- package/src/lexer/lexer.js +18 -3
- package/src/parser/animate-ast.js +45 -0
- package/src/parser/ast.js +15 -0
- package/src/parser/browser-ast.js +19 -1
- package/src/parser/browser-parser.js +221 -4
- package/src/parser/parser.js +21 -2
- package/src/parser/theme-ast.js +29 -0
- package/src/parser/theme-parser.js +70 -0
- package/src/registry/plugins/theme-plugin.js +20 -0
- package/src/registry/register-all.js +2 -0
- package/src/runtime/charts.js +547 -0
- package/src/runtime/embedded.js +6 -2
- package/src/runtime/reactivity.js +60 -0
- package/src/runtime/router.js +703 -295
- package/src/runtime/table.js +606 -33
- package/src/stdlib/inline.js +330 -7
- package/src/stdlib/string.js +84 -2
- package/src/stdlib/validation.js +1 -1
- package/src/version.js +1 -1
package/bin/tova.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { resolve, basename, dirname, join, relative } from 'path';
|
|
3
|
+
import { resolve, basename, dirname, join, relative, sep, extname } from 'path';
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, copyFileSync, rmSync, chmodSync, renameSync, watch as fsWatch } from 'fs';
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
6
|
import { createHash as _cryptoHash } from 'crypto';
|
|
7
|
+
import { createRequire as _createRequire } from 'module';
|
|
7
8
|
import { Lexer } from '../src/lexer/lexer.js';
|
|
8
9
|
import { Parser } from '../src/parser/parser.js';
|
|
9
10
|
import { Analyzer } from '../src/analyzer/analyzer.js';
|
|
@@ -14,9 +15,10 @@ import { richError, formatDiagnostics, DiagnosticFormatter, formatSummary } from
|
|
|
14
15
|
import { getExplanation, lookupCode } from '../src/diagnostics/error-codes.js';
|
|
15
16
|
import { getFullStdlib, buildSelectiveStdlib, BUILTIN_NAMES, PROPAGATE, NATIVE_INIT } from '../src/stdlib/inline.js';
|
|
16
17
|
import { Formatter } from '../src/formatter/formatter.js';
|
|
17
|
-
import { REACTIVITY_SOURCE, RPC_SOURCE, ROUTER_SOURCE } from '../src/runtime/embedded.js';
|
|
18
|
+
import { REACTIVITY_SOURCE, RPC_SOURCE, ROUTER_SOURCE, DEVTOOLS_SOURCE, SSR_SOURCE, TESTING_SOURCE } from '../src/runtime/embedded.js';
|
|
18
19
|
import '../src/runtime/string-proto.js';
|
|
19
20
|
import '../src/runtime/array-proto.js';
|
|
21
|
+
import { generateSecurityScorecard } from '../src/diagnostics/security-scorecard.js';
|
|
20
22
|
import { resolveConfig } from '../src/config/resolve.js';
|
|
21
23
|
import { writePackageJson } from '../src/config/package-json.js';
|
|
22
24
|
import { addToSection, removeFromSection } from '../src/config/edit-toml.js';
|
|
@@ -52,10 +54,10 @@ Commands:
|
|
|
52
54
|
check [dir] Type-check .tova files without generating code
|
|
53
55
|
clean Delete .tova-out build artifacts
|
|
54
56
|
dev Start development server with live reload
|
|
55
|
-
new <name> Create a new Tova project (--template fullstack|api|script|library|blank)
|
|
56
|
-
install Install
|
|
57
|
-
add <pkg> Add
|
|
58
|
-
remove <pkg> Remove
|
|
57
|
+
new <name> Create a new Tova project (--template fullstack|spa|site|api|script|library|blank)
|
|
58
|
+
install Install dependencies from tova.toml
|
|
59
|
+
add <pkg> Add a dependency (npm:pkg for npm, github.com/user/repo for Tova)
|
|
60
|
+
remove <pkg> Remove a dependency
|
|
59
61
|
repl Start interactive Tova REPL
|
|
60
62
|
lsp Start Language Server Protocol server
|
|
61
63
|
fmt <file> Format a .tova file (--check to verify only)
|
|
@@ -83,7 +85,9 @@ Options:
|
|
|
83
85
|
--verbose Show detailed output during compilation
|
|
84
86
|
--quiet Suppress non-error output
|
|
85
87
|
--debug Show verbose error output
|
|
88
|
+
--static Pre-render routes to static HTML files (used with --production)
|
|
86
89
|
--strict Enable strict type checking
|
|
90
|
+
--strict-security Promote security warnings to errors
|
|
87
91
|
`;
|
|
88
92
|
|
|
89
93
|
async function main() {
|
|
@@ -102,14 +106,15 @@ async function main() {
|
|
|
102
106
|
const command = args[0];
|
|
103
107
|
|
|
104
108
|
const isStrict = args.includes('--strict');
|
|
109
|
+
const isStrictSecurity = args.includes('--strict-security');
|
|
105
110
|
switch (command) {
|
|
106
111
|
case 'run': {
|
|
107
|
-
const runArgs = args.filter(a => a !== '--strict');
|
|
112
|
+
const runArgs = args.filter(a => a !== '--strict' && a !== '--strict-security');
|
|
108
113
|
const filePath = runArgs[1];
|
|
109
114
|
const restArgs = runArgs.slice(2);
|
|
110
115
|
const ddIdx = restArgs.indexOf('--');
|
|
111
116
|
const scriptArgs = ddIdx !== -1 ? restArgs.slice(ddIdx + 1) : restArgs;
|
|
112
|
-
await runFile(filePath, { strict: isStrict, scriptArgs });
|
|
117
|
+
await runFile(filePath, { strict: isStrict, strictSecurity: isStrictSecurity, scriptArgs });
|
|
113
118
|
break;
|
|
114
119
|
}
|
|
115
120
|
case 'build':
|
|
@@ -280,10 +285,10 @@ async function main() {
|
|
|
280
285
|
break;
|
|
281
286
|
default:
|
|
282
287
|
if (command.endsWith('.tova')) {
|
|
283
|
-
const directArgs = args.filter(a => a !== '--strict').slice(1);
|
|
288
|
+
const directArgs = args.filter(a => a !== '--strict' && a !== '--strict-security').slice(1);
|
|
284
289
|
const ddIdx = directArgs.indexOf('--');
|
|
285
290
|
const scriptArgs = ddIdx !== -1 ? directArgs.slice(ddIdx + 1) : directArgs;
|
|
286
|
-
await runFile(command, { strict: isStrict, scriptArgs });
|
|
291
|
+
await runFile(command, { strict: isStrict, strictSecurity: isStrictSecurity, scriptArgs });
|
|
287
292
|
} else {
|
|
288
293
|
console.error(`Unknown command: ${command}`);
|
|
289
294
|
console.log(HELP);
|
|
@@ -301,7 +306,7 @@ function compileTova(source, filename, options = {}) {
|
|
|
301
306
|
const parser = new Parser(tokens, filename);
|
|
302
307
|
const ast = parser.parse();
|
|
303
308
|
|
|
304
|
-
const analyzer = new Analyzer(ast, filename, { strict: options.strict || false });
|
|
309
|
+
const analyzer = new Analyzer(ast, filename, { strict: options.strict || false, strictSecurity: options.strictSecurity || false });
|
|
305
310
|
// Pre-define extra names in the analyzer scope (used by REPL for cross-line persistence)
|
|
306
311
|
if (options.knownNames) {
|
|
307
312
|
for (const name of options.knownNames) {
|
|
@@ -684,11 +689,12 @@ async function runFile(filePath, options = {}) {
|
|
|
684
689
|
}
|
|
685
690
|
const hasTovaImports = tovaImportPaths.length > 0;
|
|
686
691
|
|
|
687
|
-
const output = compileTova(source, filePath, { strict: options.strict });
|
|
692
|
+
const output = compileTova(source, filePath, { strict: options.strict, strictSecurity: options.strictSecurity });
|
|
688
693
|
|
|
689
694
|
// Execute the generated JavaScript (with stdlib)
|
|
690
695
|
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
691
696
|
const stdlib = getRunStdlib();
|
|
697
|
+
const __tova_require = _createRequire(import.meta.url);
|
|
692
698
|
|
|
693
699
|
// CLI mode: execute the cli code directly
|
|
694
700
|
if (output.isCli) {
|
|
@@ -697,8 +703,8 @@ async function runFile(filePath, options = {}) {
|
|
|
697
703
|
// Override process.argv for cli dispatch
|
|
698
704
|
const scriptArgs = options.scriptArgs || [];
|
|
699
705
|
code = `process.argv = ["node", ${JSON.stringify(resolved)}, ...${JSON.stringify(scriptArgs)}];\n` + code;
|
|
700
|
-
const fn = new AsyncFunction('__tova_args', '__tova_filename', '__tova_dirname', code);
|
|
701
|
-
await fn(scriptArgs, resolved, dirname(resolved));
|
|
706
|
+
const fn = new AsyncFunction('__tova_args', '__tova_filename', '__tova_dirname', 'require', code);
|
|
707
|
+
await fn(scriptArgs, resolved, dirname(resolved), __tova_require);
|
|
702
708
|
return;
|
|
703
709
|
}
|
|
704
710
|
|
|
@@ -733,8 +739,8 @@ async function runFile(filePath, options = {}) {
|
|
|
733
739
|
if (/\bfunction\s+main\s*\(/.test(code)) {
|
|
734
740
|
code += '\nconst __tova_exit = await main(__tova_args); if (typeof __tova_exit === "number") process.exitCode = __tova_exit;\n';
|
|
735
741
|
}
|
|
736
|
-
const fn = new AsyncFunction('__tova_args', '__tova_filename', '__tova_dirname', code);
|
|
737
|
-
await fn(scriptArgs, resolved, dirname(resolved));
|
|
742
|
+
const fn = new AsyncFunction('__tova_args', '__tova_filename', '__tova_dirname', 'require', code);
|
|
743
|
+
await fn(scriptArgs, resolved, dirname(resolved), __tova_require);
|
|
738
744
|
} catch (err) {
|
|
739
745
|
console.error(richError(source, err, filePath));
|
|
740
746
|
if (process.argv.includes('--debug') || process.env.DEBUG) {
|
|
@@ -744,12 +750,217 @@ async function runFile(filePath, options = {}) {
|
|
|
744
750
|
}
|
|
745
751
|
}
|
|
746
752
|
|
|
753
|
+
// ─── Import Path Fixup ──────────────────────────────────────
|
|
754
|
+
|
|
755
|
+
function fixImportPaths(code, outputFilePath, outDir, srcDir) {
|
|
756
|
+
const relPath = relative(outDir, outputFilePath);
|
|
757
|
+
const depth = dirname(relPath).split(sep).filter(p => p && p !== '.').length;
|
|
758
|
+
|
|
759
|
+
// Fix runtime imports: './runtime/X.js' → correct relative path based on depth
|
|
760
|
+
if (depth > 0) {
|
|
761
|
+
const prefix = '../'.repeat(depth);
|
|
762
|
+
for (const runtimeFile of ['reactivity.js', 'rpc.js', 'router.js', 'devtools.js', 'ssr.js', 'testing.js']) {
|
|
763
|
+
code = code.split("'./runtime/" + runtimeFile + "'").join("'" + prefix + "runtime/" + runtimeFile + "'");
|
|
764
|
+
code = code.split('"./runtime/' + runtimeFile + '"').join('"' + prefix + 'runtime/' + runtimeFile + '"');
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Add .js extension to relative imports that don't have one
|
|
769
|
+
code = code.replace(
|
|
770
|
+
/from\s+(['"])(\.[^'"]+)\1/g,
|
|
771
|
+
(match, quote, path) => {
|
|
772
|
+
if (path.endsWith('.js')) return match;
|
|
773
|
+
return 'from ' + quote + path + '.js' + quote;
|
|
774
|
+
}
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
// Inject missing router imports
|
|
778
|
+
code = injectRouterImport(code, depth);
|
|
779
|
+
|
|
780
|
+
// Fix duplicate identifiers between reactivity and router imports (e.g. 'lazy')
|
|
781
|
+
const reactivityMatch = code.match(/^import\s+\{([^}]+)\}\s+from\s+['"][^'"]*runtime\/reactivity[^'"]*['"]/m);
|
|
782
|
+
const routerMatch = code.match(/^(import\s+\{)([^}]+)(\}\s+from\s+['"][^'"]*runtime\/router[^'"]*['"])/m);
|
|
783
|
+
if (reactivityMatch && routerMatch) {
|
|
784
|
+
const reactivityNames = new Set(reactivityMatch[1].split(',').map(s => s.trim()));
|
|
785
|
+
const routerNames = routerMatch[2].split(',').map(s => s.trim());
|
|
786
|
+
const deduped = routerNames.filter(n => !reactivityNames.has(n));
|
|
787
|
+
if (deduped.length < routerNames.length) {
|
|
788
|
+
if (deduped.length === 0) {
|
|
789
|
+
// Remove the entire router import line if nothing left
|
|
790
|
+
code = code.replace(/^import\s+\{[^}]*\}\s+from\s+['"][^'"]*runtime\/router[^'"]*['"];?\s*\n?/m, '');
|
|
791
|
+
} else {
|
|
792
|
+
code = code.replace(routerMatch[0], routerMatch[1] + ' ' + deduped.join(', ') + ' ' + routerMatch[3]);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Fix CodeBlock/code prop template literal interpolation: the compiler treats {identifier}
|
|
798
|
+
// inside string attributes as interpolation, generating `${identifier}` in template literals.
|
|
799
|
+
// For code example strings, these should be literal braces. Revert them.
|
|
800
|
+
code = code.replace(/code:\s*`([\s\S]*?)`/g, (match, content) => {
|
|
801
|
+
if (!content.includes('${')) return match;
|
|
802
|
+
const fixed = content.replace(/\$\{(\w+)\}/g, '{ $1 }');
|
|
803
|
+
// Convert template literal to regular string with escaped quotes and newlines
|
|
804
|
+
return 'code: "' + fixed.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// File-based routing: inject routes if src/pages/ exists and no manual routes defined
|
|
808
|
+
if (srcDir && !(/\b(defineRoutes|createRouter)\s*\(/.test(code))) {
|
|
809
|
+
const fileRoutes = generateFileBasedRoutes(srcDir);
|
|
810
|
+
if (fileRoutes) {
|
|
811
|
+
// Convert Tova-style imports to JS imports with .js extensions
|
|
812
|
+
const jsRoutes = fileRoutes
|
|
813
|
+
.replace(/from\s+"([^"]+)"/g, (m, p) => 'from "' + p + '.js"');
|
|
814
|
+
// Inject before the last closing function or at the end
|
|
815
|
+
code = code + '\n// ── File-Based Routes (auto-generated from src/pages/) ──\n' + jsRoutes + '\n';
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return code;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function injectRouterImport(code, depth) {
|
|
823
|
+
const routerFuncs = ['createRouter', 'lazy', 'resetRouter', 'getPath', 'navigate',
|
|
824
|
+
'getCurrentRoute', 'getParams', 'getQuery', 'getMeta', 'getRouter',
|
|
825
|
+
'defineRoutes', 'onRouteChange', 'Router', 'Link', 'Outlet', 'Redirect',
|
|
826
|
+
'beforeNavigate', 'afterNavigate'];
|
|
827
|
+
const hasRouterImport = /runtime\/router/.test(code);
|
|
828
|
+
if (hasRouterImport) return code;
|
|
829
|
+
|
|
830
|
+
// Strip import lines before checking for router function usage to avoid false positives
|
|
831
|
+
const codeWithoutImports = code.replace(/^import\s+\{[^}]*\}\s+from\s+['"][^'"]*['"];?\s*$/gm, '');
|
|
832
|
+
const usedFuncs = routerFuncs.filter(fn => new RegExp('\\b' + fn + '\\b').test(codeWithoutImports));
|
|
833
|
+
if (usedFuncs.length === 0) return code;
|
|
834
|
+
|
|
835
|
+
const routerPath = depth === 0
|
|
836
|
+
? './runtime/router.js'
|
|
837
|
+
: '../'.repeat(depth) + 'runtime/router.js';
|
|
838
|
+
|
|
839
|
+
const importLine = "import { " + usedFuncs.join(', ') + " } from '" + routerPath + "';\n";
|
|
840
|
+
|
|
841
|
+
// Insert after first import line, or at the start
|
|
842
|
+
const firstImportEnd = code.indexOf(';\n');
|
|
843
|
+
if (firstImportEnd !== -1 && code.trimStart().startsWith('import ')) {
|
|
844
|
+
return code.slice(0, firstImportEnd + 2) + importLine + code.slice(firstImportEnd + 2);
|
|
845
|
+
}
|
|
846
|
+
return importLine + code;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// ─── File-Based Routing ──────────────────────────────────────
|
|
850
|
+
|
|
851
|
+
function generateFileBasedRoutes(srcDir) {
|
|
852
|
+
const pagesDir = join(srcDir, 'pages');
|
|
853
|
+
if (!existsSync(pagesDir) || !statSync(pagesDir).isDirectory()) return null;
|
|
854
|
+
|
|
855
|
+
// Scan pages directory recursively
|
|
856
|
+
const routes = [];
|
|
857
|
+
let hasLayout = false;
|
|
858
|
+
let has404 = false;
|
|
859
|
+
|
|
860
|
+
function scanDir(dir, prefix) {
|
|
861
|
+
const entries = readdirSync(dir).sort();
|
|
862
|
+
for (const entry of entries) {
|
|
863
|
+
const fullPath = join(dir, entry);
|
|
864
|
+
const stat = statSync(fullPath);
|
|
865
|
+
|
|
866
|
+
if (stat.isDirectory()) {
|
|
867
|
+
// Check for layout in subdirectory
|
|
868
|
+
const subLayout = join(fullPath, '_layout.tova');
|
|
869
|
+
if (existsSync(subLayout)) {
|
|
870
|
+
const childRoutes = [];
|
|
871
|
+
scanDir(fullPath, prefix + '/' + entry);
|
|
872
|
+
// Layout routes handled via nested children
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
scanDir(fullPath, prefix + '/' + entry);
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (!entry.endsWith('.tova')) continue;
|
|
880
|
+
const name = entry.replace('.tova', '');
|
|
881
|
+
|
|
882
|
+
// Skip layout files (handled separately)
|
|
883
|
+
if (name === '_layout') {
|
|
884
|
+
if (prefix === '') hasLayout = true;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// 404 page
|
|
889
|
+
if (name === '404') {
|
|
890
|
+
has404 = true;
|
|
891
|
+
const relImport = './pages' + (prefix ? prefix + '/' : '/') + name;
|
|
892
|
+
routes.push({ path: '404', importPath: relImport, componentName: 'NotFoundPage__auto' });
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Determine route path
|
|
897
|
+
let routePath;
|
|
898
|
+
if (name === 'index') {
|
|
899
|
+
routePath = prefix || '/';
|
|
900
|
+
} else if (name.startsWith('[...') && name.endsWith(']')) {
|
|
901
|
+
// Catch-all: [...slug] → *
|
|
902
|
+
routePath = prefix + '/*';
|
|
903
|
+
} else if (name.startsWith('[[') && name.endsWith(']]')) {
|
|
904
|
+
// Optional param: [[id]] → /:id?
|
|
905
|
+
const paramName = name.slice(2, -2);
|
|
906
|
+
routePath = prefix + '/:' + paramName + '?';
|
|
907
|
+
} else if (name.startsWith('[') && name.endsWith(']')) {
|
|
908
|
+
// Dynamic param: [id] → /:id
|
|
909
|
+
const paramName = name.slice(1, -1);
|
|
910
|
+
routePath = prefix + '/:' + paramName;
|
|
911
|
+
} else {
|
|
912
|
+
routePath = prefix + '/' + name;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const relImport = './pages' + (prefix ? prefix + '/' : '/') + name;
|
|
916
|
+
// Generate safe component name from path
|
|
917
|
+
const safeName = name
|
|
918
|
+
.replace(/\[\.\.\.(\w+)\]/, 'CatchAll_$1')
|
|
919
|
+
.replace(/\[\[(\w+)\]\]/, 'Optional_$1')
|
|
920
|
+
.replace(/\[(\w+)\]/, 'Param_$1')
|
|
921
|
+
.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
922
|
+
const componentName = '__Page_' + (prefix ? prefix.slice(1).replace(/\//g, '_') + '_' : '') + safeName;
|
|
923
|
+
|
|
924
|
+
routes.push({ path: routePath, importPath: relImport, componentName });
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
scanDir(pagesDir, '');
|
|
929
|
+
|
|
930
|
+
if (routes.length === 0) return null;
|
|
931
|
+
|
|
932
|
+
// Generate import statements and route map
|
|
933
|
+
const imports = routes.map(r =>
|
|
934
|
+
'import { Page as ' + r.componentName + ' } from "' + r.importPath + '"'
|
|
935
|
+
).join('\n');
|
|
936
|
+
|
|
937
|
+
const routeEntries = routes.map(r =>
|
|
938
|
+
' "' + r.path + '": ' + r.componentName + ','
|
|
939
|
+
).join('\n');
|
|
940
|
+
|
|
941
|
+
// Check for root layout
|
|
942
|
+
let layoutImport = '';
|
|
943
|
+
let layoutWrap = '';
|
|
944
|
+
if (hasLayout) {
|
|
945
|
+
layoutImport = '\nimport { Layout as __RootLayout } from "./pages/_layout"';
|
|
946
|
+
// With layout, wrap routes as children
|
|
947
|
+
// For now, just generate flat routes — layout support can be added later
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const generated = imports + layoutImport + '\n\n' +
|
|
951
|
+
'defineRoutes({\n' + routeEntries + '\n})';
|
|
952
|
+
|
|
953
|
+
return generated;
|
|
954
|
+
}
|
|
955
|
+
|
|
747
956
|
// ─── Build ──────────────────────────────────────────────────
|
|
748
957
|
|
|
749
958
|
async function buildProject(args) {
|
|
750
959
|
const config = resolveConfig(process.cwd());
|
|
751
960
|
const isProduction = args.includes('--production');
|
|
961
|
+
const isStatic = args.includes('--static');
|
|
752
962
|
const buildStrict = args.includes('--strict');
|
|
963
|
+
const buildStrictSecurity = args.includes('--strict-security');
|
|
753
964
|
const isVerbose = args.includes('--verbose');
|
|
754
965
|
const isQuiet = args.includes('--quiet');
|
|
755
966
|
const isWatch = args.includes('--watch');
|
|
@@ -767,7 +978,7 @@ async function buildProject(args) {
|
|
|
767
978
|
|
|
768
979
|
// Production build uses a separate optimized pipeline
|
|
769
980
|
if (isProduction) {
|
|
770
|
-
return await productionBuild(srcDir, outDir);
|
|
981
|
+
return await productionBuild(srcDir, outDir, isStatic);
|
|
771
982
|
}
|
|
772
983
|
|
|
773
984
|
const tovaFiles = findFiles(srcDir, '.tova');
|
|
@@ -784,6 +995,9 @@ async function buildProject(args) {
|
|
|
784
995
|
writeFileSync(join(runtimeDest, 'reactivity.js'), REACTIVITY_SOURCE);
|
|
785
996
|
writeFileSync(join(runtimeDest, 'rpc.js'), RPC_SOURCE);
|
|
786
997
|
writeFileSync(join(runtimeDest, 'router.js'), ROUTER_SOURCE);
|
|
998
|
+
writeFileSync(join(runtimeDest, 'devtools.js'), DEVTOOLS_SOURCE);
|
|
999
|
+
writeFileSync(join(runtimeDest, 'ssr.js'), SSR_SOURCE);
|
|
1000
|
+
writeFileSync(join(runtimeDest, 'testing.js'), TESTING_SOURCE);
|
|
787
1001
|
|
|
788
1002
|
if (!isQuiet) console.log(`\n Building ${tovaFiles.length} file(s)...\n`);
|
|
789
1003
|
|
|
@@ -799,6 +1013,7 @@ async function buildProject(args) {
|
|
|
799
1013
|
|
|
800
1014
|
// Group files by directory for multi-file merging
|
|
801
1015
|
const dirGroups = groupFilesByDirectory(tovaFiles);
|
|
1016
|
+
let _scorecardData = null; // Collect security info for scorecard
|
|
802
1017
|
|
|
803
1018
|
for (const [dir, files] of dirGroups) {
|
|
804
1019
|
const dirName = basename(dir) === '.' ? 'app' : basename(dir);
|
|
@@ -835,10 +1050,13 @@ async function buildProject(args) {
|
|
|
835
1050
|
}
|
|
836
1051
|
}
|
|
837
1052
|
|
|
838
|
-
const result = mergeDirectory(dir, srcDir, { strict: buildStrict });
|
|
1053
|
+
const result = mergeDirectory(dir, srcDir, { strict: buildStrict, strictSecurity: buildStrictSecurity });
|
|
839
1054
|
if (!result) continue;
|
|
840
1055
|
|
|
841
|
-
const { output, single } = result;
|
|
1056
|
+
const { output, single, warnings: buildWarnings, securityConfig, hasServer, hasEdge } = result;
|
|
1057
|
+
if ((hasServer || hasEdge) && !_scorecardData) {
|
|
1058
|
+
_scorecardData = { securityConfig, warnings: buildWarnings || [], hasServer, hasEdge };
|
|
1059
|
+
}
|
|
842
1060
|
// Preserve relative directory structure in output (e.g., src/lib/math.tova → lib/math.js)
|
|
843
1061
|
const outBaseName = single
|
|
844
1062
|
? relative(srcDir, files[0]).replace(/\.tova$/, '').replace(/\\/g, '/')
|
|
@@ -892,7 +1110,7 @@ async function buildProject(args) {
|
|
|
892
1110
|
else if (output.isModule) {
|
|
893
1111
|
if (output.shared && output.shared.trim()) {
|
|
894
1112
|
const modulePath = join(outDir, `${outBaseName}.js`);
|
|
895
|
-
writeFileSync(modulePath, generateSourceMap(output.shared, modulePath));
|
|
1113
|
+
writeFileSync(modulePath, fixImportPaths(generateSourceMap(output.shared, modulePath), modulePath, outDir));
|
|
896
1114
|
if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', modulePath)}${timing}`);
|
|
897
1115
|
}
|
|
898
1116
|
// Update incremental build cache
|
|
@@ -911,28 +1129,30 @@ async function buildProject(args) {
|
|
|
911
1129
|
// Write shared
|
|
912
1130
|
if (output.shared && output.shared.trim()) {
|
|
913
1131
|
const sharedPath = join(outDir, `${outBaseName}.shared.js`);
|
|
914
|
-
writeFileSync(sharedPath, generateSourceMap(output.shared, sharedPath));
|
|
1132
|
+
writeFileSync(sharedPath, fixImportPaths(generateSourceMap(output.shared, sharedPath), sharedPath, outDir));
|
|
915
1133
|
if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', sharedPath)}${timing}`);
|
|
916
1134
|
}
|
|
917
1135
|
|
|
918
1136
|
// Write default server
|
|
919
1137
|
if (output.server) {
|
|
920
1138
|
const serverPath = join(outDir, `${outBaseName}.server.js`);
|
|
921
|
-
writeFileSync(serverPath, generateSourceMap(output.server, serverPath));
|
|
1139
|
+
writeFileSync(serverPath, fixImportPaths(generateSourceMap(output.server, serverPath), serverPath, outDir));
|
|
922
1140
|
if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', serverPath)}${timing}`);
|
|
923
1141
|
}
|
|
924
1142
|
|
|
925
1143
|
// Write default browser
|
|
926
1144
|
if (output.browser) {
|
|
927
1145
|
const browserPath = join(outDir, `${outBaseName}.browser.js`);
|
|
928
|
-
|
|
1146
|
+
// Pass srcDir for file-based routing injection (only for root-level browser output)
|
|
1147
|
+
const browserSrcDir = (relDir === '.' || relDir === '') ? srcDir : undefined;
|
|
1148
|
+
writeFileSync(browserPath, fixImportPaths(generateSourceMap(output.browser, browserPath), browserPath, outDir, browserSrcDir));
|
|
929
1149
|
if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', browserPath)}${timing}`);
|
|
930
1150
|
}
|
|
931
1151
|
|
|
932
1152
|
// Write default edge
|
|
933
1153
|
if (output.edge) {
|
|
934
1154
|
const edgePath = join(outDir, `${outBaseName}.edge.js`);
|
|
935
|
-
writeFileSync(edgePath, generateSourceMap(output.edge, edgePath));
|
|
1155
|
+
writeFileSync(edgePath, fixImportPaths(generateSourceMap(output.edge, edgePath), edgePath, outDir));
|
|
936
1156
|
if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', edgePath)} [edge]${timing}`);
|
|
937
1157
|
}
|
|
938
1158
|
|
|
@@ -941,7 +1161,7 @@ async function buildProject(args) {
|
|
|
941
1161
|
for (const [name, code] of Object.entries(output.servers)) {
|
|
942
1162
|
if (name === 'default') continue;
|
|
943
1163
|
const path = join(outDir, `${outBaseName}.server.${name}.js`);
|
|
944
|
-
writeFileSync(path, code);
|
|
1164
|
+
writeFileSync(path, fixImportPaths(code, path, outDir));
|
|
945
1165
|
if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', path)} [server:${name}]${timing}`);
|
|
946
1166
|
}
|
|
947
1167
|
}
|
|
@@ -951,7 +1171,7 @@ async function buildProject(args) {
|
|
|
951
1171
|
for (const [name, code] of Object.entries(output.edges)) {
|
|
952
1172
|
if (name === 'default') continue;
|
|
953
1173
|
const path = join(outDir, `${outBaseName}.edge.${name}.js`);
|
|
954
|
-
writeFileSync(path, code);
|
|
1174
|
+
writeFileSync(path, fixImportPaths(code, path, outDir));
|
|
955
1175
|
if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', path)} [edge:${name}]${timing}`);
|
|
956
1176
|
}
|
|
957
1177
|
}
|
|
@@ -961,7 +1181,7 @@ async function buildProject(args) {
|
|
|
961
1181
|
for (const [name, code] of Object.entries(output.browsers)) {
|
|
962
1182
|
if (name === 'default') continue;
|
|
963
1183
|
const path = join(outDir, `${outBaseName}.browser.${name}.js`);
|
|
964
|
-
writeFileSync(path, code);
|
|
1184
|
+
writeFileSync(path, fixImportPaths(code, path, outDir));
|
|
965
1185
|
if (!isQuiet) console.log(` ✓ ${relLabel} → ${relative('.', path)} [browser:${name}]${timing}`);
|
|
966
1186
|
}
|
|
967
1187
|
}
|
|
@@ -1001,6 +1221,18 @@ async function buildProject(args) {
|
|
|
1001
1221
|
const cachedStr = skippedCount > 0 ? ` (${skippedCount} cached)` : '';
|
|
1002
1222
|
console.log(`\n Build complete. ${dirCount - errorCount}/${dirCount} directory group(s) succeeded${cachedStr}${timingStr}.\n`);
|
|
1003
1223
|
}
|
|
1224
|
+
|
|
1225
|
+
// Security scorecard (shown with --verbose or --strict-security, suppressed with --quiet)
|
|
1226
|
+
if ((isVerbose || buildStrictSecurity) && !isQuiet && _scorecardData) {
|
|
1227
|
+
const scorecard = generateSecurityScorecard(
|
|
1228
|
+
_scorecardData.securityConfig,
|
|
1229
|
+
_scorecardData.warnings,
|
|
1230
|
+
_scorecardData.hasServer,
|
|
1231
|
+
_scorecardData.hasEdge
|
|
1232
|
+
);
|
|
1233
|
+
if (scorecard) console.log(scorecard.format());
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1004
1236
|
if (errorCount > 0) process.exit(1);
|
|
1005
1237
|
|
|
1006
1238
|
// Watch mode for build command
|
|
@@ -1027,6 +1259,7 @@ async function buildProject(args) {
|
|
|
1027
1259
|
|
|
1028
1260
|
async function checkProject(args) {
|
|
1029
1261
|
const checkStrict = args.includes('--strict');
|
|
1262
|
+
const checkStrictSecurity = args.includes('--strict-security');
|
|
1030
1263
|
const isVerbose = args.includes('--verbose');
|
|
1031
1264
|
const isQuiet = args.includes('--quiet');
|
|
1032
1265
|
|
|
@@ -1069,6 +1302,8 @@ async function checkProject(args) {
|
|
|
1069
1302
|
let totalErrors = 0;
|
|
1070
1303
|
let totalWarnings = 0;
|
|
1071
1304
|
const seenCodes = new Set();
|
|
1305
|
+
let _checkScorecardData = null;
|
|
1306
|
+
const _allCheckWarnings = [];
|
|
1072
1307
|
|
|
1073
1308
|
for (const file of tovaFiles) {
|
|
1074
1309
|
const relPath = relative(srcDir, file);
|
|
@@ -1079,13 +1314,39 @@ async function checkProject(args) {
|
|
|
1079
1314
|
const tokens = lexer.tokenize();
|
|
1080
1315
|
const parser = new Parser(tokens, file);
|
|
1081
1316
|
const ast = parser.parse();
|
|
1082
|
-
const analyzer = new Analyzer(ast, file, { strict: checkStrict, tolerant: true });
|
|
1317
|
+
const analyzer = new Analyzer(ast, file, { strict: checkStrict, strictSecurity: checkStrictSecurity, tolerant: true });
|
|
1083
1318
|
const result = analyzer.analyze();
|
|
1084
1319
|
|
|
1085
1320
|
const errors = result.errors || [];
|
|
1086
1321
|
const warnings = result.warnings || [];
|
|
1087
1322
|
totalErrors += errors.length;
|
|
1088
1323
|
totalWarnings += warnings.length;
|
|
1324
|
+
_allCheckWarnings.push(...warnings);
|
|
1325
|
+
|
|
1326
|
+
// Collect security info for scorecard
|
|
1327
|
+
if (!_checkScorecardData) {
|
|
1328
|
+
const hasServer = ast.body.some(n => n.type === 'ServerBlock');
|
|
1329
|
+
const hasEdge = ast.body.some(n => n.type === 'EdgeBlock');
|
|
1330
|
+
if (hasServer || hasEdge) {
|
|
1331
|
+
const secNode = ast.body.find(n => n.type === 'SecurityBlock');
|
|
1332
|
+
let secCfg = null;
|
|
1333
|
+
if (secNode) {
|
|
1334
|
+
secCfg = {};
|
|
1335
|
+
for (const child of secNode.body || []) {
|
|
1336
|
+
if (child.type === 'AuthDeclaration') secCfg.auth = { authType: child.authType || 'jwt', storage: child.config?.storage?.value };
|
|
1337
|
+
else if (child.type === 'CsrfDeclaration') secCfg.csrf = { enabled: child.config?.enabled?.value !== false };
|
|
1338
|
+
else if (child.type === 'RateLimitDeclaration') secCfg.rateLimit = { max: child.config?.max?.value };
|
|
1339
|
+
else if (child.type === 'CspDeclaration') secCfg.csp = { default_src: true };
|
|
1340
|
+
else if (child.type === 'CorsDeclaration') {
|
|
1341
|
+
const origins = child.config?.origins;
|
|
1342
|
+
secCfg.cors = { origins: origins ? (origins.elements || []).map(e => e.value) : [] };
|
|
1343
|
+
}
|
|
1344
|
+
else if (child.type === 'AuditDeclaration') secCfg.audit = { events: ['auth'] };
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
_checkScorecardData = { securityConfig: secCfg, hasServer, hasEdge };
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1089
1350
|
|
|
1090
1351
|
if (errors.length > 0 || warnings.length > 0) {
|
|
1091
1352
|
const formatter = new DiagnosticFormatter(source, file);
|
|
@@ -1114,6 +1375,17 @@ async function checkProject(args) {
|
|
|
1114
1375
|
}
|
|
1115
1376
|
}
|
|
1116
1377
|
|
|
1378
|
+
// Security scorecard (shown with --verbose or --strict-security, suppressed with --quiet)
|
|
1379
|
+
if ((isVerbose || checkStrictSecurity) && !isQuiet && _checkScorecardData) {
|
|
1380
|
+
const scorecard = generateSecurityScorecard(
|
|
1381
|
+
_checkScorecardData.securityConfig,
|
|
1382
|
+
_allCheckWarnings,
|
|
1383
|
+
_checkScorecardData.hasServer,
|
|
1384
|
+
_checkScorecardData.hasEdge
|
|
1385
|
+
);
|
|
1386
|
+
if (scorecard) console.log(scorecard.format());
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1117
1389
|
if (!isQuiet) {
|
|
1118
1390
|
console.log(`\n ${tovaFiles.length} file${tovaFiles.length === 1 ? '' : 's'} checked, ${formatSummary(totalErrors, totalWarnings)}`);
|
|
1119
1391
|
// Show explain hint for encountered error codes
|
|
@@ -1150,6 +1422,7 @@ async function devServer(args) {
|
|
|
1150
1422
|
const explicitPort = args.find((_, i, a) => a[i - 1] === '--port');
|
|
1151
1423
|
const basePort = parseInt(explicitPort || config.dev.port || '3000');
|
|
1152
1424
|
const buildStrict = args.includes('--strict');
|
|
1425
|
+
const buildStrictSecurity = args.includes('--strict-security');
|
|
1153
1426
|
|
|
1154
1427
|
const tovaFiles = findFiles(srcDir, '.tova');
|
|
1155
1428
|
if (tovaFiles.length === 0) {
|
|
@@ -1193,6 +1466,9 @@ async function devServer(args) {
|
|
|
1193
1466
|
writeFileSync(join(runtimeDest, 'reactivity.js'), REACTIVITY_SOURCE);
|
|
1194
1467
|
writeFileSync(join(runtimeDest, 'rpc.js'), RPC_SOURCE);
|
|
1195
1468
|
writeFileSync(join(runtimeDest, 'router.js'), ROUTER_SOURCE);
|
|
1469
|
+
writeFileSync(join(runtimeDest, 'devtools.js'), DEVTOOLS_SOURCE);
|
|
1470
|
+
writeFileSync(join(runtimeDest, 'ssr.js'), SSR_SOURCE);
|
|
1471
|
+
writeFileSync(join(runtimeDest, 'testing.js'), TESTING_SOURCE);
|
|
1196
1472
|
|
|
1197
1473
|
const serverFiles = [];
|
|
1198
1474
|
let hasClient = false;
|
|
@@ -1208,25 +1484,40 @@ async function devServer(args) {
|
|
|
1208
1484
|
|
|
1209
1485
|
// Pass 1: Merge each directory, write shared/client outputs, collect clientHTML
|
|
1210
1486
|
const dirResults = [];
|
|
1487
|
+
const allSharedParts = [];
|
|
1488
|
+
let browserCode = '';
|
|
1211
1489
|
for (const [dir, files] of dirGroups) {
|
|
1212
1490
|
const dirName = basename(dir) === '.' ? 'app' : basename(dir);
|
|
1213
1491
|
try {
|
|
1214
|
-
const result = mergeDirectory(dir, srcDir, { strict: buildStrict });
|
|
1492
|
+
const result = mergeDirectory(dir, srcDir, { strict: buildStrict, strictSecurity: buildStrictSecurity });
|
|
1215
1493
|
if (!result) continue;
|
|
1216
1494
|
|
|
1217
1495
|
const { output, single } = result;
|
|
1218
|
-
const
|
|
1496
|
+
const relDir = relative(srcDir, dir);
|
|
1497
|
+
const outBaseName = single
|
|
1498
|
+
? relative(srcDir, files[0]).replace(/\.tova$/, '').replace(/\\/g, '/')
|
|
1499
|
+
: (relDir === '.' ? dirName : relDir + '/' + dirName);
|
|
1219
1500
|
dirResults.push({ dir, output, outBaseName, single, files });
|
|
1220
1501
|
|
|
1502
|
+
// Ensure output subdirectory exists for nested paths
|
|
1503
|
+
const outSubDir = dirname(join(outDir, outBaseName));
|
|
1504
|
+
if (outSubDir !== outDir) mkdirSync(outSubDir, { recursive: true });
|
|
1505
|
+
|
|
1221
1506
|
if (output.shared && output.shared.trim()) {
|
|
1222
|
-
|
|
1507
|
+
// Use .js (not .shared.js) for module files to match build output
|
|
1508
|
+
const ext = (output.isModule || (!output.browser && !output.server)) ? '.js' : '.shared.js';
|
|
1509
|
+
const sp = join(outDir, `${outBaseName}${ext}`);
|
|
1510
|
+
const fixedShared = fixImportPaths(output.shared, sp, outDir);
|
|
1511
|
+
writeFileSync(sp, fixedShared);
|
|
1512
|
+
allSharedParts.push(fixedShared);
|
|
1223
1513
|
}
|
|
1224
1514
|
|
|
1225
1515
|
if (output.browser) {
|
|
1226
1516
|
const p = join(outDir, `${outBaseName}.browser.js`);
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
writeFileSync(
|
|
1517
|
+
const browserSrcDir = (relative(srcDir, dir) === '.' || relative(srcDir, dir) === '') ? srcDir : undefined;
|
|
1518
|
+
const fixedBrowser = fixImportPaths(output.browser, p, outDir, browserSrcDir);
|
|
1519
|
+
writeFileSync(p, fixedBrowser);
|
|
1520
|
+
browserCode = fixedBrowser;
|
|
1230
1521
|
hasClient = true;
|
|
1231
1522
|
}
|
|
1232
1523
|
} catch (err) {
|
|
@@ -1234,6 +1525,16 @@ async function devServer(args) {
|
|
|
1234
1525
|
}
|
|
1235
1526
|
}
|
|
1236
1527
|
|
|
1528
|
+
// Generate dev HTML with all shared code prepended to browser code
|
|
1529
|
+
// Skip if the project has its own index.html (uses import maps or custom module loading)
|
|
1530
|
+
const hasCustomIndex = existsSync(join(process.cwd(), 'index.html'));
|
|
1531
|
+
if (hasClient && !hasCustomIndex) {
|
|
1532
|
+
const allSharedCode = allSharedParts.join('\n').replace(/^export /gm, '');
|
|
1533
|
+
const fullClientCode = allSharedCode ? allSharedCode + '\n' + browserCode : browserCode;
|
|
1534
|
+
clientHTML = await generateDevHTML(fullClientCode, srcDir, actualReloadPort);
|
|
1535
|
+
writeFileSync(join(outDir, 'index.html'), clientHTML);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1237
1538
|
// Pass 2: Write server files with clientHTML injected
|
|
1238
1539
|
for (const { output, outBaseName } of dirResults) {
|
|
1239
1540
|
if (output.server) {
|
|
@@ -1305,6 +1606,75 @@ async function devServer(args) {
|
|
|
1305
1606
|
console.log(` ✓ Client: ${relative('.', outDir)}/index.html`);
|
|
1306
1607
|
}
|
|
1307
1608
|
|
|
1609
|
+
// If no server blocks were found but we have a client, start a static file server
|
|
1610
|
+
if (processes.length === 0 && hasClient) {
|
|
1611
|
+
const mimeTypes = {
|
|
1612
|
+
'.html': 'text/html',
|
|
1613
|
+
'.js': 'application/javascript',
|
|
1614
|
+
'.mjs': 'application/javascript',
|
|
1615
|
+
'.css': 'text/css',
|
|
1616
|
+
'.json': 'application/json',
|
|
1617
|
+
'.png': 'image/png',
|
|
1618
|
+
'.jpg': 'image/jpeg',
|
|
1619
|
+
'.gif': 'image/gif',
|
|
1620
|
+
'.svg': 'image/svg+xml',
|
|
1621
|
+
'.ico': 'image/x-icon',
|
|
1622
|
+
'.woff': 'font/woff',
|
|
1623
|
+
'.woff2': 'font/woff2',
|
|
1624
|
+
'.map': 'application/json',
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
const staticServer = Bun.serve({
|
|
1628
|
+
port: basePort,
|
|
1629
|
+
async fetch(req) {
|
|
1630
|
+
const url = new URL(req.url);
|
|
1631
|
+
let pathname = url.pathname;
|
|
1632
|
+
|
|
1633
|
+
// Try to serve the file directly from outDir, srcDir, or project root
|
|
1634
|
+
const tryPaths = [
|
|
1635
|
+
join(outDir, pathname),
|
|
1636
|
+
join(srcDir, pathname),
|
|
1637
|
+
join(process.cwd(), pathname),
|
|
1638
|
+
];
|
|
1639
|
+
|
|
1640
|
+
for (const filePath of tryPaths) {
|
|
1641
|
+
if (existsSync(filePath) && statSync(filePath).isFile()) {
|
|
1642
|
+
const ext = extname(filePath);
|
|
1643
|
+
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
1644
|
+
const content = readFileSync(filePath);
|
|
1645
|
+
return new Response(content, {
|
|
1646
|
+
headers: {
|
|
1647
|
+
'Content-Type': contentType,
|
|
1648
|
+
'Cache-Control': 'no-cache',
|
|
1649
|
+
'Access-Control-Allow-Origin': '*',
|
|
1650
|
+
},
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// SPA fallback: serve index.html for non-file routes
|
|
1656
|
+
const indexPath = join(outDir, 'index.html');
|
|
1657
|
+
if (existsSync(indexPath)) {
|
|
1658
|
+
return new Response(readFileSync(indexPath), {
|
|
1659
|
+
headers: { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' },
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
const rootIndex = join(process.cwd(), 'index.html');
|
|
1664
|
+
if (existsSync(rootIndex)) {
|
|
1665
|
+
return new Response(readFileSync(rootIndex), {
|
|
1666
|
+
headers: { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' },
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
return new Response('Not Found', { status: 404 });
|
|
1671
|
+
},
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
console.log(`\n Static file server running:`);
|
|
1675
|
+
console.log(` → http://localhost:${basePort}`);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1308
1678
|
function handleReloadFetch(req) {
|
|
1309
1679
|
const url = new URL(req.url);
|
|
1310
1680
|
if (url.pathname === '/__tova_reload') {
|
|
@@ -1358,22 +1728,39 @@ async function devServer(args) {
|
|
|
1358
1728
|
// Merge each directory group, collect client HTML
|
|
1359
1729
|
const rebuildDirGroups = groupFilesByDirectory(currentFiles);
|
|
1360
1730
|
let rebuildClientHTML = '';
|
|
1731
|
+
const rebuildSharedParts = [];
|
|
1732
|
+
let rebuildBrowserCode = '';
|
|
1733
|
+
let rebuildHasClient = false;
|
|
1361
1734
|
|
|
1362
1735
|
for (const [dir, files] of rebuildDirGroups) {
|
|
1363
1736
|
const dirName = basename(dir) === '.' ? 'app' : basename(dir);
|
|
1364
|
-
const result = mergeDirectory(dir, srcDir, { strict: buildStrict });
|
|
1737
|
+
const result = mergeDirectory(dir, srcDir, { strict: buildStrict, strictSecurity: buildStrictSecurity });
|
|
1365
1738
|
if (!result) continue;
|
|
1366
1739
|
|
|
1367
1740
|
const { output, single } = result;
|
|
1368
|
-
const
|
|
1741
|
+
const relDir = relative(srcDir, dir);
|
|
1742
|
+
const outBaseName = single
|
|
1743
|
+
? relative(srcDir, files[0]).replace(/\.tova$/, '').replace(/\\/g, '/')
|
|
1744
|
+
: (relDir === '.' ? dirName : relDir + '/' + dirName);
|
|
1745
|
+
|
|
1746
|
+
// Ensure output subdirectory exists for nested paths
|
|
1747
|
+
const outSubDir = dirname(join(outDir, outBaseName));
|
|
1748
|
+
if (outSubDir !== outDir) mkdirSync(outSubDir, { recursive: true });
|
|
1369
1749
|
|
|
1370
1750
|
if (output.shared && output.shared.trim()) {
|
|
1371
|
-
|
|
1751
|
+
const ext = (output.isModule || (!output.browser && !output.server)) ? '.js' : '.shared.js';
|
|
1752
|
+
const sp = join(outDir, `${outBaseName}${ext}`);
|
|
1753
|
+
const fixedShared = fixImportPaths(output.shared, sp, outDir);
|
|
1754
|
+
writeFileSync(sp, fixedShared);
|
|
1755
|
+
rebuildSharedParts.push(fixedShared);
|
|
1372
1756
|
}
|
|
1373
1757
|
if (output.browser) {
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1758
|
+
const p = join(outDir, `${outBaseName}.browser.js`);
|
|
1759
|
+
const browserSrcDir = (relative(srcDir, dir) === '.' || relative(srcDir, dir) === '') ? srcDir : undefined;
|
|
1760
|
+
const fixedBrowser = fixImportPaths(output.browser, p, outDir, browserSrcDir);
|
|
1761
|
+
writeFileSync(p, fixedBrowser);
|
|
1762
|
+
rebuildBrowserCode = fixedBrowser;
|
|
1763
|
+
rebuildHasClient = true;
|
|
1377
1764
|
}
|
|
1378
1765
|
if (output.server) {
|
|
1379
1766
|
let serverCode = output.server;
|
|
@@ -1381,7 +1768,7 @@ async function devServer(args) {
|
|
|
1381
1768
|
serverCode = `const __clientHTML = ${JSON.stringify(rebuildClientHTML)};\n` + serverCode;
|
|
1382
1769
|
}
|
|
1383
1770
|
const p = join(outDir, `${outBaseName}.server.js`);
|
|
1384
|
-
writeFileSync(p, serverCode);
|
|
1771
|
+
writeFileSync(p, fixImportPaths(serverCode, p, outDir));
|
|
1385
1772
|
newServerFiles.push(p);
|
|
1386
1773
|
}
|
|
1387
1774
|
if (output.multiBlock && output.servers) {
|
|
@@ -1393,6 +1780,14 @@ async function devServer(args) {
|
|
|
1393
1780
|
}
|
|
1394
1781
|
}
|
|
1395
1782
|
}
|
|
1783
|
+
|
|
1784
|
+
// Generate dev HTML with all shared code prepended to browser code
|
|
1785
|
+
if (rebuildHasClient) {
|
|
1786
|
+
const rebuildAllShared = rebuildSharedParts.join('\n').replace(/^export /gm, '');
|
|
1787
|
+
const rebuildFullClient = rebuildAllShared ? rebuildAllShared + '\n' + rebuildBrowserCode : rebuildBrowserCode;
|
|
1788
|
+
rebuildClientHTML = await generateDevHTML(rebuildFullClient, srcDir, actualReloadPort);
|
|
1789
|
+
writeFileSync(join(outDir, 'index.html'), rebuildClientHTML);
|
|
1790
|
+
}
|
|
1396
1791
|
} catch (err) {
|
|
1397
1792
|
console.error(` ✗ Rebuild failed: ${err.message}`);
|
|
1398
1793
|
return; // Keep old processes running
|
|
@@ -1510,6 +1905,10 @@ ${bundled}
|
|
|
1510
1905
|
const inlineReactivity = REACTIVITY_SOURCE.replace(/^export /gm, '');
|
|
1511
1906
|
const inlineRpc = RPC_SOURCE.replace(/^export /gm, '');
|
|
1512
1907
|
|
|
1908
|
+
// Detect if client code uses routing (defineRoutes, Router, getPath, navigate, etc.)
|
|
1909
|
+
const usesRouter = /\b(createRouter|lazy|resetRouter|defineRoutes|Router|getPath|getQuery|getParams|getCurrentRoute|getMeta|getRouter|navigate|onRouteChange|beforeNavigate|afterNavigate|Outlet|Link|Redirect)\b/.test(clientCode);
|
|
1910
|
+
const inlineRouter = usesRouter ? ROUTER_SOURCE.replace(/^export /gm, '').replace(/^\s*import\s+(?:\{[^}]*\}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"][^'"]+['"];?\s*$/gm, '') : '';
|
|
1911
|
+
|
|
1513
1912
|
// Strip all import lines from client code (we inline the runtime instead)
|
|
1514
1913
|
const inlineClient = clientCode
|
|
1515
1914
|
.replace(/^\s*import\s+(?:\{[^}]*\}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"][^'"]+['"];?\s*$/gm, '')
|
|
@@ -1560,6 +1959,8 @@ ${inlineReactivity}
|
|
|
1560
1959
|
// ── Tova Runtime: RPC ──
|
|
1561
1960
|
${inlineRpc}
|
|
1562
1961
|
|
|
1962
|
+
${usesRouter ? '// ── Tova Runtime: Router ──\n' + inlineRouter : ''}
|
|
1963
|
+
|
|
1563
1964
|
// ── App ──
|
|
1564
1965
|
${inlineClient}
|
|
1565
1966
|
</script>${liveReloadScript}
|
|
@@ -1574,11 +1975,12 @@ ${inlineClient}
|
|
|
1574
1975
|
const PROJECT_TEMPLATES = {
|
|
1575
1976
|
fullstack: {
|
|
1576
1977
|
label: 'Full-stack app',
|
|
1577
|
-
description: 'server +
|
|
1978
|
+
description: 'server + browser + shared blocks',
|
|
1578
1979
|
tomlDescription: 'A full-stack Tova application',
|
|
1579
1980
|
entry: 'src',
|
|
1580
1981
|
file: 'src/app.tova',
|
|
1581
1982
|
content: name => `// ${name} — Built with Tova
|
|
1983
|
+
// Full-stack app: server RPC + client-side routing
|
|
1582
1984
|
|
|
1583
1985
|
shared {
|
|
1584
1986
|
type Message {
|
|
@@ -1595,7 +1997,7 @@ server {
|
|
|
1595
1997
|
route GET "/api/message" => get_message
|
|
1596
1998
|
}
|
|
1597
1999
|
|
|
1598
|
-
|
|
2000
|
+
browser {
|
|
1599
2001
|
state message = ""
|
|
1600
2002
|
state timestamp = ""
|
|
1601
2003
|
state refreshing = false
|
|
@@ -1614,6 +2016,23 @@ client {
|
|
|
1614
2016
|
refreshing = false
|
|
1615
2017
|
}
|
|
1616
2018
|
|
|
2019
|
+
// ─── Navigation ─────────────────────────────────────────
|
|
2020
|
+
component NavBar {
|
|
2021
|
+
<nav class="border-b border-gray-100 bg-white/80 backdrop-blur-sm sticky top-0 z-10">
|
|
2022
|
+
<div class="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
|
|
2023
|
+
<Link href="/" class="flex items-center gap-2 no-underline">
|
|
2024
|
+
<div class="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg"></div>
|
|
2025
|
+
<span class="font-bold text-gray-900 text-lg">"${name}"</span>
|
|
2026
|
+
</Link>
|
|
2027
|
+
<div class="flex items-center gap-6">
|
|
2028
|
+
<Link href="/" exactActiveClass="text-indigo-600 font-semibold" class="text-sm font-medium transition-colors text-gray-500 hover:text-gray-900 no-underline">"Home"</Link>
|
|
2029
|
+
<Link href="/about" activeClass="text-indigo-600 font-semibold" class="text-sm font-medium transition-colors text-gray-500 hover:text-gray-900 no-underline">"About"</Link>
|
|
2030
|
+
</div>
|
|
2031
|
+
</div>
|
|
2032
|
+
</nav>
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// ─── Pages ──────────────────────────────────────────────
|
|
1617
2036
|
component FeatureCard(icon, title, description) {
|
|
1618
2037
|
<div class="group relative bg-white rounded-2xl p-6 shadow-sm border border-gray-100 hover:shadow-lg hover:border-indigo-100 transition-all duration-300">
|
|
1619
2038
|
<div class="w-10 h-10 bg-indigo-50 rounded-xl flex items-center justify-center text-lg mb-4 group-hover:bg-indigo-100 transition-colors">
|
|
@@ -1624,72 +2043,416 @@ client {
|
|
|
1624
2043
|
</div>
|
|
1625
2044
|
}
|
|
1626
2045
|
|
|
2046
|
+
component HomePage {
|
|
2047
|
+
<main class="max-w-5xl mx-auto px-6">
|
|
2048
|
+
<div class="py-20 text-center">
|
|
2049
|
+
<div class="inline-flex items-center gap-2 bg-indigo-50 text-indigo-700 text-sm font-medium px-4 py-1.5 rounded-full mb-6">
|
|
2050
|
+
<span class="w-1.5 h-1.5 bg-indigo-500 rounded-full"></span>
|
|
2051
|
+
"Powered by Tova"
|
|
2052
|
+
</div>
|
|
2053
|
+
<h1 class="text-5xl font-bold text-gray-900 tracking-tight mb-4">"Welcome to " <span class="bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">"${name}"</span></h1>
|
|
2054
|
+
<p class="text-xl text-gray-500 max-w-2xl mx-auto mb-10">"A modern full-stack app. Edit " <code class="text-sm bg-gray-100 text-indigo-600 px-2 py-1 rounded-md font-mono">"src/app.tova"</code> " to get started."</p>
|
|
2055
|
+
|
|
2056
|
+
<div class="inline-flex items-center gap-3 bg-white border border-gray-200 rounded-2xl p-2 shadow-sm">
|
|
2057
|
+
<div class="bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-5 py-2.5 rounded-xl font-medium">
|
|
2058
|
+
"{message}"
|
|
2059
|
+
</div>
|
|
2060
|
+
<button
|
|
2061
|
+
on:click={handle_refresh}
|
|
2062
|
+
class="px-4 py-2.5 text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 rounded-xl transition-all font-medium text-sm"
|
|
2063
|
+
>
|
|
2064
|
+
if refreshing {
|
|
2065
|
+
"..."
|
|
2066
|
+
} else {
|
|
2067
|
+
"Refresh"
|
|
2068
|
+
}
|
|
2069
|
+
</button>
|
|
2070
|
+
</div>
|
|
2071
|
+
if timestamp != "" {
|
|
2072
|
+
<p class="text-xs text-gray-400 mt-3">"Last fetched at " "{timestamp}"</p>
|
|
2073
|
+
}
|
|
2074
|
+
</div>
|
|
2075
|
+
|
|
2076
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-5 pb-20">
|
|
2077
|
+
<FeatureCard
|
|
2078
|
+
icon="\u2699"
|
|
2079
|
+
title="Full-Stack"
|
|
2080
|
+
description="Server and client in one file. Shared types, RPC calls, and reactive UI — all type-safe."
|
|
2081
|
+
/>
|
|
2082
|
+
<FeatureCard
|
|
2083
|
+
icon="\u26A1"
|
|
2084
|
+
title="Fast Refresh"
|
|
2085
|
+
description="Edit your code and see changes instantly. The dev server recompiles on save."
|
|
2086
|
+
/>
|
|
2087
|
+
<FeatureCard
|
|
2088
|
+
icon="\uD83C\uDFA8"
|
|
2089
|
+
title="Tailwind Built-in"
|
|
2090
|
+
description="Style with utility classes out of the box. No config or build step needed."
|
|
2091
|
+
/>
|
|
2092
|
+
</div>
|
|
2093
|
+
</main>
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
component AboutPage {
|
|
2097
|
+
<main class="max-w-5xl mx-auto px-6 py-12">
|
|
2098
|
+
<h2 class="text-3xl font-bold text-gray-900 mb-6">"About"</h2>
|
|
2099
|
+
<div class="bg-white rounded-xl border border-gray-200 p-8 space-y-4">
|
|
2100
|
+
<p class="text-gray-600 leading-relaxed">"${name} is a full-stack application built with Tova — a modern language that compiles to JavaScript."</p>
|
|
2101
|
+
<p class="text-gray-600 leading-relaxed">"It uses shared types between server and browser, server-side RPC, and client-side routing."</p>
|
|
2102
|
+
</div>
|
|
2103
|
+
<div class="mt-8">
|
|
2104
|
+
<Link href="/" class="text-indigo-600 hover:text-indigo-700 font-medium no-underline">"\u2190 Back to home"</Link>
|
|
2105
|
+
</div>
|
|
2106
|
+
</main>
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
component NotFoundPage {
|
|
2110
|
+
<div class="max-w-5xl mx-auto px-6 py-16 text-center">
|
|
2111
|
+
<h1 class="text-6xl font-bold text-gray-200 mb-4">"404"</h1>
|
|
2112
|
+
<p class="text-lg text-gray-500 mb-6">"Page not found"</p>
|
|
2113
|
+
<Link href="/" class="text-indigo-600 hover:text-indigo-700 font-medium no-underline">"Go home"</Link>
|
|
2114
|
+
</div>
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// ─── Router setup ─────────────────────────────────────────
|
|
2118
|
+
createRouter({
|
|
2119
|
+
routes: {
|
|
2120
|
+
"/": HomePage,
|
|
2121
|
+
"/about": { component: AboutPage, meta: { title: "About" } },
|
|
2122
|
+
"404": NotFoundPage,
|
|
2123
|
+
},
|
|
2124
|
+
scroll: "auto",
|
|
2125
|
+
})
|
|
2126
|
+
|
|
1627
2127
|
component App {
|
|
1628
2128
|
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-white to-indigo-50">
|
|
1629
|
-
<
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
2129
|
+
<NavBar />
|
|
2130
|
+
<Router />
|
|
2131
|
+
<div class="border-t border-gray-100 py-8 text-center">
|
|
2132
|
+
<p class="text-sm text-gray-400">"Built with " <a href="https://github.com/tova-lang/tova-lang" class="text-indigo-500 hover:text-indigo-600 transition-colors">"Tova"</a></p>
|
|
2133
|
+
</div>
|
|
2134
|
+
</div>
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
`,
|
|
2138
|
+
nextSteps: name => ` cd ${name}\n tova dev`,
|
|
2139
|
+
},
|
|
2140
|
+
spa: {
|
|
2141
|
+
label: 'Single-page app',
|
|
2142
|
+
description: 'browser-only app with routing',
|
|
2143
|
+
tomlDescription: 'A Tova single-page application',
|
|
2144
|
+
entry: 'src',
|
|
2145
|
+
file: 'src/app.tova',
|
|
2146
|
+
content: name => `// ${name} — Built with Tova
|
|
2147
|
+
// Demonstrates: createRouter, Link, Router, Outlet, navigate(),
|
|
2148
|
+
// dynamic :param routes, nested routes, route meta, 404 handling
|
|
2149
|
+
|
|
2150
|
+
browser {
|
|
2151
|
+
// ─── Navigation bar with active link highlighting ─────────
|
|
2152
|
+
component NavBar {
|
|
2153
|
+
<nav class="bg-white border-b border-gray-100 sticky top-0 z-10">
|
|
2154
|
+
<div class="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
|
|
2155
|
+
<Link href="/" class="flex items-center gap-2 no-underline">
|
|
2156
|
+
<div class="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg"></div>
|
|
2157
|
+
<span class="font-bold text-gray-900 text-lg">"${name}"</span>
|
|
2158
|
+
</Link>
|
|
2159
|
+
<div class="flex items-center gap-6">
|
|
2160
|
+
<Link href="/" exactActiveClass="text-indigo-600 font-semibold" class="text-sm font-medium transition-colors text-gray-500 hover:text-gray-900">"Home"</Link>
|
|
2161
|
+
<Link href="/users" activeClass="text-indigo-600 font-semibold" class="text-sm font-medium transition-colors text-gray-500 hover:text-gray-900">"Users"</Link>
|
|
2162
|
+
<Link href="/settings" activeClass="text-indigo-600 font-semibold" class="text-sm font-medium transition-colors text-gray-500 hover:text-gray-900">"Settings"</Link>
|
|
2163
|
+
</div>
|
|
2164
|
+
</div>
|
|
2165
|
+
</nav>
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// ─── Home page ────────────────────────────────────────────
|
|
2169
|
+
component HomePage {
|
|
2170
|
+
<div class="max-w-5xl mx-auto px-6 py-16 text-center">
|
|
2171
|
+
<div class="inline-flex items-center gap-2 bg-indigo-50 text-indigo-700 text-sm font-medium px-4 py-1.5 rounded-full mb-6">
|
|
2172
|
+
<span class="w-1.5 h-1.5 bg-indigo-500 rounded-full"></span>
|
|
2173
|
+
"Tova Router"
|
|
2174
|
+
</div>
|
|
2175
|
+
<h1 class="text-4xl font-bold text-gray-900 mb-4">"Welcome to " <span class="text-indigo-600">"${name}"</span></h1>
|
|
2176
|
+
<p class="text-lg text-gray-500 mb-8">"A single-page app with client-side routing. Click around to explore."</p>
|
|
2177
|
+
<div class="flex items-center justify-center gap-4">
|
|
2178
|
+
<Link href="/users" class="inline-block bg-indigo-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-indigo-700 transition-colors no-underline">"Browse Users"</Link>
|
|
2179
|
+
<Link href="/settings/profile" class="inline-block bg-white text-gray-700 border border-gray-200 px-6 py-3 rounded-lg font-medium hover:bg-gray-50 transition-colors no-underline">"Settings"</Link>
|
|
2180
|
+
</div>
|
|
2181
|
+
</div>
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// ─── Users list (demonstrates programmatic navigation) ────
|
|
2185
|
+
fn go_to_user(uid) {
|
|
2186
|
+
navigate("/users/{uid}")
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
component UsersPage {
|
|
2190
|
+
<div class="max-w-5xl mx-auto px-6 py-12">
|
|
2191
|
+
<h2 class="text-2xl font-bold text-gray-900 mb-6">"Users"</h2>
|
|
2192
|
+
<div class="bg-white rounded-xl border border-gray-200 divide-y divide-gray-100">
|
|
2193
|
+
<div class="flex items-center justify-between p-4 hover:bg-gray-50 cursor-pointer transition-colors" on:click={fn() go_to_user("1")}>
|
|
2194
|
+
<div class="flex items-center gap-3">
|
|
2195
|
+
<div class="w-9 h-9 bg-indigo-100 text-indigo-600 rounded-full flex items-center justify-center font-semibold text-sm">"A"</div>
|
|
2196
|
+
<div>
|
|
2197
|
+
<p class="font-medium text-gray-900">"alice"</p>
|
|
2198
|
+
<p class="text-xs text-gray-500">"Admin"</p>
|
|
2199
|
+
</div>
|
|
2200
|
+
</div>
|
|
2201
|
+
<span class="text-gray-400 text-sm">"View \u2192"</span>
|
|
2202
|
+
</div>
|
|
2203
|
+
<div class="flex items-center justify-between p-4 hover:bg-gray-50 cursor-pointer transition-colors" on:click={fn() go_to_user("2")}>
|
|
2204
|
+
<div class="flex items-center gap-3">
|
|
2205
|
+
<div class="w-9 h-9 bg-green-100 text-green-600 rounded-full flex items-center justify-center font-semibold text-sm">"B"</div>
|
|
2206
|
+
<div>
|
|
2207
|
+
<p class="font-medium text-gray-900">"bob"</p>
|
|
2208
|
+
<p class="text-xs text-gray-500">"Editor"</p>
|
|
2209
|
+
</div>
|
|
1634
2210
|
</div>
|
|
1635
|
-
<
|
|
1636
|
-
|
|
1637
|
-
|
|
2211
|
+
<span class="text-gray-400 text-sm">"View \u2192"</span>
|
|
2212
|
+
</div>
|
|
2213
|
+
<div class="flex items-center justify-between p-4 hover:bg-gray-50 cursor-pointer transition-colors" on:click={fn() go_to_user("3")}>
|
|
2214
|
+
<div class="flex items-center gap-3">
|
|
2215
|
+
<div class="w-9 h-9 bg-purple-100 text-purple-600 rounded-full flex items-center justify-center font-semibold text-sm">"C"</div>
|
|
2216
|
+
<div>
|
|
2217
|
+
<p class="font-medium text-gray-900">"charlie"</p>
|
|
2218
|
+
<p class="text-xs text-gray-500">"Viewer"</p>
|
|
2219
|
+
</div>
|
|
1638
2220
|
</div>
|
|
2221
|
+
<span class="text-gray-400 text-sm">"View \u2192"</span>
|
|
1639
2222
|
</div>
|
|
1640
|
-
</
|
|
2223
|
+
</div>
|
|
2224
|
+
</div>
|
|
2225
|
+
}
|
|
1641
2226
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
2227
|
+
// ─── User detail (demonstrates :id dynamic route param) ───
|
|
2228
|
+
component UserPage(id) {
|
|
2229
|
+
<div class="max-w-5xl mx-auto px-6 py-12">
|
|
2230
|
+
<button on:click={fn() navigate("/users")} class="text-sm text-indigo-600 hover:text-indigo-700 mb-6 inline-flex items-center gap-1 cursor-pointer bg-transparent border-0">
|
|
2231
|
+
"\u2190 Back to users"
|
|
2232
|
+
</button>
|
|
2233
|
+
<div class="bg-white rounded-xl border border-gray-200 p-8">
|
|
2234
|
+
<div class="flex items-center gap-4 mb-6">
|
|
2235
|
+
<div class="w-14 h-14 bg-indigo-100 text-indigo-600 rounded-full flex items-center justify-center font-bold text-xl">
|
|
2236
|
+
"#{id}"
|
|
1647
2237
|
</div>
|
|
1648
|
-
<
|
|
1649
|
-
|
|
2238
|
+
<div>
|
|
2239
|
+
<h2 class="text-2xl font-bold text-gray-900">"User {id}"</h2>
|
|
2240
|
+
<span class="text-sm text-gray-500">"Dynamic route parameter: " <code class="bg-gray-100 text-indigo-600 px-1.5 py-0.5 rounded text-xs">":id = {id}"</code></span>
|
|
2241
|
+
</div>
|
|
2242
|
+
</div>
|
|
2243
|
+
<p class="text-gray-600">"This page receives " <code class="bg-gray-100 text-indigo-600 px-1.5 py-0.5 rounded text-xs">"id"</code> " from the route " <code class="bg-gray-100 text-indigo-600 px-1.5 py-0.5 rounded text-xs">"/users/:id"</code> " pattern."</p>
|
|
2244
|
+
</div>
|
|
2245
|
+
</div>
|
|
2246
|
+
}
|
|
1650
2247
|
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
>
|
|
1659
|
-
|
|
1660
|
-
"..."
|
|
1661
|
-
} else {
|
|
1662
|
-
"Refresh"
|
|
1663
|
-
}
|
|
1664
|
-
</button>
|
|
2248
|
+
// ─── Settings layout with nested routes + Outlet ──────────
|
|
2249
|
+
component SettingsLayout {
|
|
2250
|
+
<div class="max-w-5xl mx-auto px-6 py-12">
|
|
2251
|
+
<h2 class="text-2xl font-bold text-gray-900 mb-6">"Settings"</h2>
|
|
2252
|
+
<div class="flex gap-8">
|
|
2253
|
+
<aside class="w-48 flex-shrink-0">
|
|
2254
|
+
<div class="flex flex-col gap-1">
|
|
2255
|
+
<Link href="/settings/profile" activeClass="bg-indigo-50 text-indigo-700" class="block px-3 py-2 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-50 no-underline transition-colors">"Profile"</Link>
|
|
2256
|
+
<Link href="/settings/account" activeClass="bg-indigo-50 text-indigo-700" class="block px-3 py-2 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-50 no-underline transition-colors">"Account"</Link>
|
|
1665
2257
|
</div>
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
2258
|
+
</aside>
|
|
2259
|
+
<div class="flex-1 min-w-0">
|
|
2260
|
+
<Outlet />
|
|
1669
2261
|
</div>
|
|
2262
|
+
</div>
|
|
2263
|
+
</div>
|
|
2264
|
+
}
|
|
1670
2265
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
<
|
|
1678
|
-
|
|
1679
|
-
title="Fast Refresh"
|
|
1680
|
-
description="Edit your code and see changes instantly. The dev server recompiles on save."
|
|
1681
|
-
/>
|
|
1682
|
-
<FeatureCard
|
|
1683
|
-
icon="🎨"
|
|
1684
|
-
title="Tailwind Built-in"
|
|
1685
|
-
description="Style with utility classes out of the box. No config or build step needed."
|
|
1686
|
-
/>
|
|
2266
|
+
component ProfileSettings {
|
|
2267
|
+
<div class="bg-white rounded-xl border border-gray-200 p-6">
|
|
2268
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">"Profile Settings"</h3>
|
|
2269
|
+
<p class="text-gray-600 mb-4">"This is a nested child route rendered via " <code class="bg-gray-100 text-indigo-600 px-1.5 py-0.5 rounded text-xs">"Outlet"</code> " inside SettingsLayout."</p>
|
|
2270
|
+
<div class="space-y-4">
|
|
2271
|
+
<div>
|
|
2272
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">"Display Name"</label>
|
|
2273
|
+
<div class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm text-gray-900">"Alice"</div>
|
|
1687
2274
|
</div>
|
|
2275
|
+
<div>
|
|
2276
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">"Bio"</label>
|
|
2277
|
+
<div class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm text-gray-500">"Tova developer"</div>
|
|
2278
|
+
</div>
|
|
2279
|
+
</div>
|
|
2280
|
+
</div>
|
|
2281
|
+
}
|
|
1688
2282
|
|
|
1689
|
-
|
|
1690
|
-
|
|
2283
|
+
component AccountSettings {
|
|
2284
|
+
<div class="bg-white rounded-xl border border-gray-200 p-6">
|
|
2285
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">"Account Settings"</h3>
|
|
2286
|
+
<p class="text-gray-600 mb-4">"Another nested child of " <code class="bg-gray-100 text-indigo-600 px-1.5 py-0.5 rounded text-xs">"/settings"</code> "."</p>
|
|
2287
|
+
<div class="space-y-4">
|
|
2288
|
+
<div>
|
|
2289
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">"Email"</label>
|
|
2290
|
+
<div class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm text-gray-900">"alice@example.com"</div>
|
|
2291
|
+
</div>
|
|
2292
|
+
<div class="pt-4 border-t border-gray-100">
|
|
2293
|
+
<button class="text-sm text-red-600 hover:text-red-700 font-medium cursor-pointer bg-transparent border-0">"Delete Account"</button>
|
|
1691
2294
|
</div>
|
|
2295
|
+
</div>
|
|
2296
|
+
</div>
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// ─── 404 page ─────────────────────────────────────────────
|
|
2300
|
+
component NotFoundPage {
|
|
2301
|
+
<div class="max-w-5xl mx-auto px-6 py-16 text-center">
|
|
2302
|
+
<h1 class="text-6xl font-bold text-gray-200 mb-4">"404"</h1>
|
|
2303
|
+
<p class="text-lg text-gray-500 mb-6">"Page not found"</p>
|
|
2304
|
+
<Link href="/" class="text-indigo-600 hover:text-indigo-700 font-medium no-underline">"Go home"</Link>
|
|
2305
|
+
</div>
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// ─── Router setup ─────────────────────────────────────────
|
|
2309
|
+
createRouter({
|
|
2310
|
+
routes: {
|
|
2311
|
+
"/": HomePage,
|
|
2312
|
+
"/users": { component: UsersPage, meta: { title: "Users" } },
|
|
2313
|
+
"/users/:id": { component: UserPage, meta: { title: "User Detail" } },
|
|
2314
|
+
"/settings": {
|
|
2315
|
+
component: SettingsLayout,
|
|
2316
|
+
children: {
|
|
2317
|
+
"/profile": { component: ProfileSettings, meta: { title: "Profile" } },
|
|
2318
|
+
"/account": { component: AccountSettings, meta: { title: "Account" } },
|
|
2319
|
+
},
|
|
2320
|
+
},
|
|
2321
|
+
"404": NotFoundPage,
|
|
2322
|
+
},
|
|
2323
|
+
scroll: "auto",
|
|
2324
|
+
})
|
|
2325
|
+
|
|
2326
|
+
// ─── Update document title from route meta ────────────────
|
|
2327
|
+
afterNavigate(fn(current) {
|
|
2328
|
+
if current.meta != undefined {
|
|
2329
|
+
if current.meta.title != undefined {
|
|
2330
|
+
document.title = "{current.meta.title} | ${name}"
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
})
|
|
2334
|
+
|
|
2335
|
+
component App {
|
|
2336
|
+
<div class="min-h-screen bg-gray-50">
|
|
2337
|
+
<NavBar />
|
|
2338
|
+
<Router />
|
|
2339
|
+
</div>
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
`,
|
|
2343
|
+
nextSteps: name => ` cd ${name}\n tova dev`,
|
|
2344
|
+
},
|
|
2345
|
+
site: {
|
|
2346
|
+
label: 'Static site',
|
|
2347
|
+
description: 'docs or marketing site with pages',
|
|
2348
|
+
tomlDescription: 'A Tova static site',
|
|
2349
|
+
entry: 'src',
|
|
2350
|
+
file: 'src/app.tova',
|
|
2351
|
+
extraFiles: [
|
|
2352
|
+
{
|
|
2353
|
+
path: 'src/pages/home.tova',
|
|
2354
|
+
content: name => `pub component HomePage {
|
|
2355
|
+
<div class="max-w-4xl mx-auto px-6 py-16">
|
|
2356
|
+
<h1 class="text-4xl font-bold text-gray-900 mb-4">"Welcome to ${name}"</h1>
|
|
2357
|
+
<p class="text-lg text-gray-600 mb-8">"A static site built with Tova. Fast, simple, and easy to deploy anywhere."</p>
|
|
2358
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
2359
|
+
<div class="bg-white rounded-xl border border-gray-200 p-6">
|
|
2360
|
+
<h3 class="font-semibold text-gray-900 mb-2">"Fast by default"</h3>
|
|
2361
|
+
<p class="text-gray-500 text-sm">"Client-side routing for smooth, instant navigation between pages."</p>
|
|
2362
|
+
</div>
|
|
2363
|
+
<div class="bg-white rounded-xl border border-gray-200 p-6">
|
|
2364
|
+
<h3 class="font-semibold text-gray-900 mb-2">"Deploy anywhere"</h3>
|
|
2365
|
+
<p class="text-gray-500 text-sm">"GitHub Pages, Netlify, Vercel, Firebase — works with any static host."</p>
|
|
2366
|
+
</div>
|
|
2367
|
+
</div>
|
|
2368
|
+
</div>
|
|
2369
|
+
}
|
|
2370
|
+
`,
|
|
2371
|
+
},
|
|
2372
|
+
{
|
|
2373
|
+
path: 'src/pages/docs.tova',
|
|
2374
|
+
content: name => `pub component DocsPage {
|
|
2375
|
+
<div class="max-w-4xl mx-auto px-6 py-12">
|
|
2376
|
+
<h1 class="text-3xl font-bold text-gray-900 mb-6">"Documentation"</h1>
|
|
2377
|
+
<div class="prose">
|
|
2378
|
+
<h2 class="text-xl font-semibold text-gray-900 mt-8 mb-3">"Getting Started"</h2>
|
|
2379
|
+
<p class="text-gray-600 mb-4">"Add your documentation content here. Each page is a Tova component with its own route."</p>
|
|
2380
|
+
<h2 class="text-xl font-semibold text-gray-900 mt-8 mb-3">"Adding Pages"</h2>
|
|
2381
|
+
<p class="text-gray-600 mb-4">"Create a new file in " <code class="bg-gray-100 text-indigo-600 px-1.5 py-0.5 rounded text-sm">"src/pages/"</code> " and add a route in " <code class="bg-gray-100 text-indigo-600 px-1.5 py-0.5 rounded text-sm">"src/app.tova"</code> "."</p>
|
|
2382
|
+
</div>
|
|
2383
|
+
</div>
|
|
2384
|
+
}
|
|
2385
|
+
`,
|
|
2386
|
+
},
|
|
2387
|
+
{
|
|
2388
|
+
path: 'src/pages/about.tova',
|
|
2389
|
+
content: name => `pub component AboutPage {
|
|
2390
|
+
<div class="max-w-4xl mx-auto px-6 py-12">
|
|
2391
|
+
<h1 class="text-3xl font-bold text-gray-900 mb-6">"About"</h1>
|
|
2392
|
+
<p class="text-gray-600">"This site was built with Tova — a modern programming language that compiles to JavaScript."</p>
|
|
2393
|
+
</div>
|
|
2394
|
+
}
|
|
2395
|
+
`,
|
|
2396
|
+
},
|
|
2397
|
+
],
|
|
2398
|
+
content: name => `// ${name} — Built with Tova
|
|
2399
|
+
import { HomePage } from "./pages/home"
|
|
2400
|
+
import { DocsPage } from "./pages/docs"
|
|
2401
|
+
import { AboutPage } from "./pages/about"
|
|
2402
|
+
|
|
2403
|
+
browser {
|
|
2404
|
+
component SiteNav {
|
|
2405
|
+
<header class="bg-white border-b border-gray-100 sticky top-0 z-10">
|
|
2406
|
+
<div class="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
|
|
2407
|
+
<Link href="/" class="flex items-center gap-2 no-underline">
|
|
2408
|
+
<div class="w-7 h-7 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg"></div>
|
|
2409
|
+
<span class="font-bold text-gray-900">"${name}"</span>
|
|
2410
|
+
</Link>
|
|
2411
|
+
<nav class="flex items-center gap-6">
|
|
2412
|
+
<Link href="/" exactActiveClass="text-indigo-600 font-semibold" class="text-sm font-medium transition-colors no-underline text-gray-500 hover:text-gray-900">"Home"</Link>
|
|
2413
|
+
<Link href="/docs" activeClass="text-indigo-600 font-semibold" class="text-sm font-medium transition-colors no-underline text-gray-500 hover:text-gray-900">"Docs"</Link>
|
|
2414
|
+
<Link href="/about" activeClass="text-indigo-600 font-semibold" class="text-sm font-medium transition-colors no-underline text-gray-500 hover:text-gray-900">"About"</Link>
|
|
2415
|
+
</nav>
|
|
2416
|
+
</div>
|
|
2417
|
+
</header>
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
component NotFoundPage {
|
|
2421
|
+
<div class="max-w-4xl mx-auto px-6 py-16 text-center">
|
|
2422
|
+
<h1 class="text-6xl font-bold text-gray-200 mb-4">"404"</h1>
|
|
2423
|
+
<p class="text-lg text-gray-500 mb-6">"Page not found"</p>
|
|
2424
|
+
<Link href="/" class="text-indigo-600 hover:text-indigo-700 font-medium no-underline">"Go home"</Link>
|
|
2425
|
+
</div>
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
createRouter({
|
|
2429
|
+
routes: {
|
|
2430
|
+
"/": HomePage,
|
|
2431
|
+
"/docs": { component: DocsPage, meta: { title: "Documentation" } },
|
|
2432
|
+
"/about": { component: AboutPage, meta: { title: "About" } },
|
|
2433
|
+
"404": NotFoundPage,
|
|
2434
|
+
},
|
|
2435
|
+
scroll: "auto",
|
|
2436
|
+
})
|
|
2437
|
+
|
|
2438
|
+
// Update document title from route meta
|
|
2439
|
+
afterNavigate(fn(current) {
|
|
2440
|
+
if current.meta != undefined {
|
|
2441
|
+
if current.meta.title != undefined {
|
|
2442
|
+
document.title = "{current.meta.title} | ${name}"
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
})
|
|
2446
|
+
|
|
2447
|
+
component App {
|
|
2448
|
+
<div class="min-h-screen bg-gray-50">
|
|
2449
|
+
<SiteNav />
|
|
2450
|
+
<main>
|
|
2451
|
+
<Router />
|
|
1692
2452
|
</main>
|
|
2453
|
+
<footer class="border-t border-gray-100 py-8 text-center">
|
|
2454
|
+
<p class="text-sm text-gray-400">"Built with Tova"</p>
|
|
2455
|
+
</footer>
|
|
1693
2456
|
</div>
|
|
1694
2457
|
}
|
|
1695
2458
|
}
|
|
@@ -1733,12 +2496,20 @@ print("Hello, {name}!")
|
|
|
1733
2496
|
tomlDescription: 'A Tova library',
|
|
1734
2497
|
entry: 'src',
|
|
1735
2498
|
noEntry: true,
|
|
2499
|
+
isPackage: true,
|
|
1736
2500
|
file: 'src/lib.tova',
|
|
1737
2501
|
content: name => `// ${name} — A Tova library
|
|
2502
|
+
//
|
|
2503
|
+
// Usage:
|
|
2504
|
+
// import { greet } from "github.com/yourname/${name}"
|
|
1738
2505
|
|
|
1739
2506
|
pub fn greet(name: String) -> String {
|
|
1740
2507
|
"Hello, {name}!"
|
|
1741
2508
|
}
|
|
2509
|
+
|
|
2510
|
+
pub fn version() -> String {
|
|
2511
|
+
"0.1.0"
|
|
2512
|
+
}
|
|
1742
2513
|
`,
|
|
1743
2514
|
nextSteps: name => ` cd ${name}\n tova build`,
|
|
1744
2515
|
},
|
|
@@ -1753,7 +2524,7 @@ pub fn greet(name: String) -> String {
|
|
|
1753
2524
|
},
|
|
1754
2525
|
};
|
|
1755
2526
|
|
|
1756
|
-
const TEMPLATE_ORDER = ['fullstack', 'api', 'script', 'library', 'blank'];
|
|
2527
|
+
const TEMPLATE_ORDER = ['fullstack', 'spa', 'site', 'api', 'script', 'library', 'blank'];
|
|
1757
2528
|
|
|
1758
2529
|
async function newProject(rawArgs) {
|
|
1759
2530
|
const name = rawArgs.find(a => !a.startsWith('-'));
|
|
@@ -1770,11 +2541,12 @@ async function newProject(rawArgs) {
|
|
|
1770
2541
|
|
|
1771
2542
|
if (!name) {
|
|
1772
2543
|
console.error(color.red('Error: No project name specified'));
|
|
1773
|
-
console.error('Usage: tova new <project-name> [--template fullstack|api|script|library|blank]');
|
|
2544
|
+
console.error('Usage: tova new <project-name> [--template fullstack|spa|site|api|script|library|blank]');
|
|
1774
2545
|
process.exit(1);
|
|
1775
2546
|
}
|
|
1776
2547
|
|
|
1777
2548
|
const projectDir = resolve(name);
|
|
2549
|
+
const projectName = basename(projectDir);
|
|
1778
2550
|
if (existsSync(projectDir)) {
|
|
1779
2551
|
console.error(color.red(`Error: Directory '${name}' already exists`));
|
|
1780
2552
|
process.exit(1);
|
|
@@ -1826,24 +2598,49 @@ async function newProject(rawArgs) {
|
|
|
1826
2598
|
const createdFiles = [];
|
|
1827
2599
|
|
|
1828
2600
|
// tova.toml
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2601
|
+
let tomlContent;
|
|
2602
|
+
if (template.isPackage) {
|
|
2603
|
+
// Library packages use [package] section per package management design
|
|
2604
|
+
tomlContent = [
|
|
2605
|
+
'[package]',
|
|
2606
|
+
`name = "github.com/yourname/${projectName}"`,
|
|
2607
|
+
`version = "0.1.0"`,
|
|
2608
|
+
`description = "${template.tomlDescription}"`,
|
|
2609
|
+
`license = "MIT"`,
|
|
2610
|
+
`exports = ["greet", "version"]`,
|
|
2611
|
+
'',
|
|
2612
|
+
'[build]',
|
|
2613
|
+
'output = ".tova-out"',
|
|
2614
|
+
'',
|
|
2615
|
+
'[dependencies]',
|
|
2616
|
+
'',
|
|
2617
|
+
'[npm]',
|
|
2618
|
+
'',
|
|
2619
|
+
].join('\n') + '\n';
|
|
2620
|
+
} else {
|
|
2621
|
+
const tomlConfig = {
|
|
2622
|
+
project: {
|
|
2623
|
+
name: projectName,
|
|
2624
|
+
version: '0.1.0',
|
|
2625
|
+
description: template.tomlDescription,
|
|
2626
|
+
},
|
|
2627
|
+
build: {
|
|
2628
|
+
output: '.tova-out',
|
|
2629
|
+
},
|
|
2630
|
+
};
|
|
2631
|
+
if (!template.noEntry) {
|
|
2632
|
+
tomlConfig.project.entry = template.entry;
|
|
2633
|
+
}
|
|
2634
|
+
if (templateName === 'fullstack' || templateName === 'api' || templateName === 'spa' || templateName === 'site') {
|
|
2635
|
+
tomlConfig.dev = { port: 3000 };
|
|
2636
|
+
tomlConfig.npm = {};
|
|
2637
|
+
}
|
|
2638
|
+
if (templateName === 'spa' || templateName === 'site') {
|
|
2639
|
+
tomlConfig.deploy = { base: '/' };
|
|
2640
|
+
}
|
|
2641
|
+
tomlContent = stringifyTOML(tomlConfig);
|
|
1845
2642
|
}
|
|
1846
|
-
writeFileSync(join(projectDir, 'tova.toml'),
|
|
2643
|
+
writeFileSync(join(projectDir, 'tova.toml'), tomlContent);
|
|
1847
2644
|
createdFiles.push('tova.toml');
|
|
1848
2645
|
|
|
1849
2646
|
// .gitignore
|
|
@@ -1859,12 +2656,22 @@ bun.lock
|
|
|
1859
2656
|
|
|
1860
2657
|
// Template source file
|
|
1861
2658
|
if (template.file && template.content) {
|
|
1862
|
-
writeFileSync(join(projectDir, template.file), template.content(
|
|
2659
|
+
writeFileSync(join(projectDir, template.file), template.content(projectName));
|
|
1863
2660
|
createdFiles.push(template.file);
|
|
1864
2661
|
}
|
|
1865
2662
|
|
|
2663
|
+
// Extra files (e.g., page components for site template)
|
|
2664
|
+
if (template.extraFiles) {
|
|
2665
|
+
for (const extra of template.extraFiles) {
|
|
2666
|
+
const extraPath = join(projectDir, extra.path);
|
|
2667
|
+
mkdirSync(dirname(extraPath), { recursive: true });
|
|
2668
|
+
writeFileSync(extraPath, extra.content(projectName));
|
|
2669
|
+
createdFiles.push(extra.path);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
|
|
1866
2673
|
// README
|
|
1867
|
-
|
|
2674
|
+
let readmeContent = `# ${projectName}
|
|
1868
2675
|
|
|
1869
2676
|
Built with [Tova](https://github.com/tova-lang/tova-lang) — a modern full-stack language.
|
|
1870
2677
|
|
|
@@ -1873,7 +2680,34 @@ Built with [Tova](https://github.com/tova-lang/tova-lang) — a modern full-stac
|
|
|
1873
2680
|
\`\`\`bash
|
|
1874
2681
|
${template.nextSteps(name).trim()}
|
|
1875
2682
|
\`\`\`
|
|
1876
|
-
|
|
2683
|
+
`;
|
|
2684
|
+
if (template.isPackage) {
|
|
2685
|
+
readmeContent += `
|
|
2686
|
+
## Usage
|
|
2687
|
+
|
|
2688
|
+
\`\`\`tova
|
|
2689
|
+
import { greet } from "github.com/yourname/${projectName}"
|
|
2690
|
+
|
|
2691
|
+
print(greet("world"))
|
|
2692
|
+
\`\`\`
|
|
2693
|
+
|
|
2694
|
+
## Publishing
|
|
2695
|
+
|
|
2696
|
+
Tag a release and push — no registry needed:
|
|
2697
|
+
|
|
2698
|
+
\`\`\`bash
|
|
2699
|
+
git tag v0.1.0
|
|
2700
|
+
git push origin v0.1.0
|
|
2701
|
+
\`\`\`
|
|
2702
|
+
|
|
2703
|
+
Others can then add your package:
|
|
2704
|
+
|
|
2705
|
+
\`\`\`bash
|
|
2706
|
+
tova add github.com/yourname/${projectName}
|
|
2707
|
+
\`\`\`
|
|
2708
|
+
`;
|
|
2709
|
+
}
|
|
2710
|
+
writeFileSync(join(projectDir, 'README.md'), readmeContent);
|
|
1877
2711
|
createdFiles.push('README.md');
|
|
1878
2712
|
|
|
1879
2713
|
// Print created files
|
|
@@ -1964,7 +2798,7 @@ server {
|
|
|
1964
2798
|
route GET "/api/message" => get_message
|
|
1965
2799
|
}
|
|
1966
2800
|
|
|
1967
|
-
|
|
2801
|
+
browser {
|
|
1968
2802
|
state message = ""
|
|
1969
2803
|
|
|
1970
2804
|
effect {
|
|
@@ -2003,8 +2837,16 @@ async function installDeps() {
|
|
|
2003
2837
|
|
|
2004
2838
|
// Resolve Tova module dependencies (if any)
|
|
2005
2839
|
const tovaDeps = config.dependencies || {};
|
|
2006
|
-
const { isTovModule: _isTovMod } = await import('../src/config/module-path.js');
|
|
2007
|
-
|
|
2840
|
+
const { isTovModule: _isTovMod, expandBlessedPackage: _expandBlessed } = await import('../src/config/module-path.js');
|
|
2841
|
+
|
|
2842
|
+
// Expand blessed package shorthands (e.g., tova/data → github.com/tova-lang/data)
|
|
2843
|
+
const expandedTovaDeps = {};
|
|
2844
|
+
for (const [k, v] of Object.entries(tovaDeps)) {
|
|
2845
|
+
const expanded = _expandBlessed(k);
|
|
2846
|
+
expandedTovaDeps[expanded || k] = v;
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
const tovModuleKeys = Object.keys(expandedTovaDeps).filter(k => _isTovMod(k));
|
|
2008
2850
|
|
|
2009
2851
|
if (tovModuleKeys.length > 0) {
|
|
2010
2852
|
const { resolveDependencies } = await import('../src/config/resolver.js');
|
|
@@ -2017,7 +2859,7 @@ async function installDeps() {
|
|
|
2017
2859
|
const lock = readLockFile(cwd);
|
|
2018
2860
|
const tovaModuleDeps = {};
|
|
2019
2861
|
for (const k of tovModuleKeys) {
|
|
2020
|
-
tovaModuleDeps[k] =
|
|
2862
|
+
tovaModuleDeps[k] = expandedTovaDeps[k];
|
|
2021
2863
|
}
|
|
2022
2864
|
|
|
2023
2865
|
try {
|
|
@@ -2132,7 +2974,7 @@ async function addDep(args) {
|
|
|
2132
2974
|
await installDeps();
|
|
2133
2975
|
} else {
|
|
2134
2976
|
// Tova native dependency
|
|
2135
|
-
const { isTovModule: isTovMod } = await import('../src/config/module-path.js');
|
|
2977
|
+
const { isTovModule: isTovMod, expandBlessedPackage } = await import('../src/config/module-path.js');
|
|
2136
2978
|
|
|
2137
2979
|
// Parse potential @version suffix
|
|
2138
2980
|
let pkgName = actualPkg;
|
|
@@ -2143,21 +2985,25 @@ async function addDep(args) {
|
|
|
2143
2985
|
pkgName = pkgName.slice(0, atIdx);
|
|
2144
2986
|
}
|
|
2145
2987
|
|
|
2988
|
+
// Expand blessed package shorthand: tova/data → github.com/tova-lang/data
|
|
2989
|
+
const expandedPkg = expandBlessedPackage(pkgName);
|
|
2990
|
+
const resolvedPkg = expandedPkg || pkgName;
|
|
2991
|
+
|
|
2146
2992
|
if (isTovMod(pkgName)) {
|
|
2147
2993
|
// Tova module: fetch tags, pick version, add to [dependencies]
|
|
2148
2994
|
const { listRemoteTags, pickLatestTag } = await import('../src/config/git-resolver.js');
|
|
2149
2995
|
try {
|
|
2150
|
-
const tags = await listRemoteTags(
|
|
2996
|
+
const tags = await listRemoteTags(resolvedPkg);
|
|
2151
2997
|
if (tags.length === 0) {
|
|
2152
|
-
console.error(` No version tags found for ${
|
|
2998
|
+
console.error(` No version tags found for ${resolvedPkg}`);
|
|
2153
2999
|
process.exit(1);
|
|
2154
3000
|
}
|
|
2155
3001
|
if (!versionConstraint) {
|
|
2156
3002
|
const latest = pickLatestTag(tags);
|
|
2157
3003
|
versionConstraint = `^${latest.version}`;
|
|
2158
3004
|
}
|
|
2159
|
-
addToSection(tomlPath, 'dependencies', `"${
|
|
2160
|
-
console.log(` Added ${
|
|
3005
|
+
addToSection(tomlPath, 'dependencies', `"${resolvedPkg}"`, versionConstraint);
|
|
3006
|
+
console.log(` Added ${resolvedPkg}@${versionConstraint} to [dependencies] in tova.toml`);
|
|
2161
3007
|
await installDeps();
|
|
2162
3008
|
} catch (err) {
|
|
2163
3009
|
console.error(` Failed to add ${pkgName}: ${err.message}`);
|
|
@@ -3010,6 +3856,26 @@ async function startRepl() {
|
|
|
3010
3856
|
const declaredInCode = new Set();
|
|
3011
3857
|
for (const m of code.matchAll(/\bfunction\s+([a-zA-Z_]\w*)/g)) { declaredInCode.add(m[1]); userDefinedNames.add(m[1]); }
|
|
3012
3858
|
for (const m of code.matchAll(/\bconst\s+([a-zA-Z_]\w*)/g)) { declaredInCode.add(m[1]); userDefinedNames.add(m[1]); }
|
|
3859
|
+
// Extract destructured names: const { a, b } = ... or const [ a, b ] = ...
|
|
3860
|
+
for (const m of code.matchAll(/\bconst\s+\{\s*([^}]+)\}/g)) {
|
|
3861
|
+
for (const part of m[1].split(',')) {
|
|
3862
|
+
const trimmed = part.trim();
|
|
3863
|
+
if (!trimmed) continue;
|
|
3864
|
+
// Handle renaming: "key: alias" or "key: alias = default" — extract the alias
|
|
3865
|
+
const colonMatch = trimmed.match(/^\w+\s*:\s*([a-zA-Z_]\w*)/);
|
|
3866
|
+
const name = colonMatch ? colonMatch[1] : trimmed.match(/^([a-zA-Z_]\w*)/)?.[1];
|
|
3867
|
+
if (name) { declaredInCode.add(name); userDefinedNames.add(name); }
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
for (const m of code.matchAll(/\bconst\s+\[\s*([^\]]+)\]/g)) {
|
|
3871
|
+
for (const part of m[1].split(',')) {
|
|
3872
|
+
const trimmed = part.trim();
|
|
3873
|
+
if (!trimmed) continue;
|
|
3874
|
+
const name = trimmed.startsWith('...') ? trimmed.slice(3).trim() : trimmed;
|
|
3875
|
+
const id = name.match(/^([a-zA-Z_]\w*)/)?.[1];
|
|
3876
|
+
if (id) { declaredInCode.add(id); userDefinedNames.add(id); }
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3013
3879
|
for (const m of code.matchAll(/\blet\s+([a-zA-Z_]\w*)/g)) {
|
|
3014
3880
|
declaredInCode.add(m[1]);
|
|
3015
3881
|
userDefinedNames.add(m[1]);
|
|
@@ -3306,7 +4172,11 @@ async function binaryBuild(srcDir, outputName, outDir) {
|
|
|
3306
4172
|
|
|
3307
4173
|
// ─── Production Build ────────────────────────────────────────
|
|
3308
4174
|
|
|
3309
|
-
async function productionBuild(srcDir, outDir) {
|
|
4175
|
+
async function productionBuild(srcDir, outDir, isStatic = false) {
|
|
4176
|
+
const config = resolveConfig(process.cwd());
|
|
4177
|
+
const basePath = config.deploy?.base || '/';
|
|
4178
|
+
const base = basePath.endsWith('/') ? basePath : basePath + '/';
|
|
4179
|
+
|
|
3310
4180
|
const tovaFiles = findFiles(srcDir, '.tova');
|
|
3311
4181
|
if (tovaFiles.length === 0) {
|
|
3312
4182
|
console.error('No .tova files found');
|
|
@@ -3351,6 +4221,9 @@ async function productionBuild(srcDir, outDir) {
|
|
|
3351
4221
|
const serverPath = join(outDir, `server.${hash}.js`);
|
|
3352
4222
|
writeFileSync(serverPath, serverBundle);
|
|
3353
4223
|
console.log(` server.${hash}.js`);
|
|
4224
|
+
|
|
4225
|
+
// Write stable server.js entrypoint for Docker/deployment
|
|
4226
|
+
writeFileSync(join(outDir, 'server.js'), `import "./server.${hash}.js";\n`);
|
|
3354
4227
|
}
|
|
3355
4228
|
|
|
3356
4229
|
// Write script bundle for plain scripts (no server/client blocks)
|
|
@@ -3378,7 +4251,9 @@ async function productionBuild(srcDir, outDir) {
|
|
|
3378
4251
|
// No npm imports — inline runtime, strip all imports
|
|
3379
4252
|
const reactivityCode = REACTIVITY_SOURCE.replace(/^export /gm, '');
|
|
3380
4253
|
const rpcCode = RPC_SOURCE.replace(/^export /gm, '');
|
|
3381
|
-
|
|
4254
|
+
const usesRouter = /\b(defineRoutes|Router|getPath|getQuery|getParams|getCurrentRoute|navigate|onRouteChange|beforeNavigate|afterNavigate|Outlet|Link|Redirect)\b/.test(allClientCode);
|
|
4255
|
+
const routerCode = usesRouter ? ROUTER_SOURCE.replace(/^export /gm, '').replace(/^\s*import\s+(?:\{[^}]*\}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"][^'"]+['"];?\s*$/gm, '') : '';
|
|
4256
|
+
clientBundle = reactivityCode + '\n' + rpcCode + '\n' + (routerCode ? routerCode + '\n' : '') + allSharedCode + '\n' +
|
|
3382
4257
|
allClientCode.replace(/^\s*import\s+(?:\{[^}]*\}|[\w$]+|\*\s+as\s+[\w$]+)\s+from\s+['"][^'"]+['"];?\s*$/gm, '').trim();
|
|
3383
4258
|
}
|
|
3384
4259
|
|
|
@@ -3389,14 +4264,19 @@ async function productionBuild(srcDir, outDir) {
|
|
|
3389
4264
|
|
|
3390
4265
|
// Generate production HTML
|
|
3391
4266
|
const scriptTag = useModule
|
|
3392
|
-
? `<script type="module" src="client.${hash}.js"></script>`
|
|
3393
|
-
: `<script src="client.${hash}.js"></script>`;
|
|
4267
|
+
? `<script type="module" src="${base}.tova-out/client.${hash}.js"></script>`
|
|
4268
|
+
: `<script src="${base}.tova-out/client.${hash}.js"></script>`;
|
|
3394
4269
|
const html = `<!DOCTYPE html>
|
|
3395
4270
|
<html lang="en">
|
|
3396
4271
|
<head>
|
|
3397
4272
|
<meta charset="UTF-8">
|
|
3398
4273
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3399
4274
|
<title>Tova App</title>
|
|
4275
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
4276
|
+
<style>
|
|
4277
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
4278
|
+
body { font-family: system-ui, -apple-system, sans-serif; }
|
|
4279
|
+
</style>
|
|
3400
4280
|
</head>
|
|
3401
4281
|
<body>
|
|
3402
4282
|
<div id="app"></div>
|
|
@@ -3405,6 +4285,12 @@ async function productionBuild(srcDir, outDir) {
|
|
|
3405
4285
|
</html>`;
|
|
3406
4286
|
writeFileSync(join(outDir, 'index.html'), html);
|
|
3407
4287
|
console.log(` index.html`);
|
|
4288
|
+
|
|
4289
|
+
// SPA fallback files for various static hosts
|
|
4290
|
+
writeFileSync(join(outDir, '404.html'), html);
|
|
4291
|
+
console.log(` 404.html (GitHub Pages SPA fallback)`);
|
|
4292
|
+
writeFileSync(join(outDir, '200.html'), html);
|
|
4293
|
+
console.log(` 200.html (Surge SPA fallback)`);
|
|
3408
4294
|
}
|
|
3409
4295
|
|
|
3410
4296
|
// Minify all JS bundles using Bun's built-in transpiler
|
|
@@ -3441,13 +4327,64 @@ async function productionBuild(srcDir, outDir) {
|
|
|
3441
4327
|
}
|
|
3442
4328
|
}
|
|
3443
4329
|
|
|
4330
|
+
// Rewrite min entrypoints to import minified hashed files
|
|
4331
|
+
for (const f of ['server.min.js', 'script.min.js']) {
|
|
4332
|
+
const minEntry = join(outDir, f);
|
|
4333
|
+
try {
|
|
4334
|
+
const content = readFileSync(minEntry, 'utf-8');
|
|
4335
|
+
const rewritten = content.replace(/\.js(["'])/g, '.min.js$1');
|
|
4336
|
+
writeFileSync(minEntry, rewritten);
|
|
4337
|
+
} catch {}
|
|
4338
|
+
}
|
|
4339
|
+
|
|
3444
4340
|
if (minified === 0 && jsFiles.length > 0) {
|
|
3445
4341
|
console.log(' (minification skipped — Bun.build unavailable)');
|
|
3446
4342
|
}
|
|
3447
4343
|
|
|
4344
|
+
// Static generation: pre-render each route to its own HTML file
|
|
4345
|
+
if (isStatic && allClientCode.trim()) {
|
|
4346
|
+
console.log(`\n Static generation...\n`);
|
|
4347
|
+
|
|
4348
|
+
const routePaths = extractRoutePaths(allClientCode);
|
|
4349
|
+
if (routePaths.length > 0) {
|
|
4350
|
+
// Read the generated index.html to use as the shell for all routes
|
|
4351
|
+
const shellHtml = readFileSync(join(outDir, 'index.html'), 'utf-8');
|
|
4352
|
+
for (const routePath of routePaths) {
|
|
4353
|
+
const htmlPath = routePath === '/'
|
|
4354
|
+
? join(outDir, 'index.html')
|
|
4355
|
+
: join(outDir, routePath.replace(/^\//, ''), 'index.html');
|
|
4356
|
+
|
|
4357
|
+
mkdirSync(dirname(htmlPath), { recursive: true });
|
|
4358
|
+
writeFileSync(htmlPath, shellHtml);
|
|
4359
|
+
const relPath = relative(outDir, htmlPath);
|
|
4360
|
+
console.log(` ${relPath}`);
|
|
4361
|
+
}
|
|
4362
|
+
console.log(`\n Pre-rendered ${routePaths.length} route(s)`);
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
|
|
3448
4366
|
console.log(`\n Production build complete.\n`);
|
|
3449
4367
|
}
|
|
3450
4368
|
|
|
4369
|
+
function extractRoutePaths(code) {
|
|
4370
|
+
// Support both defineRoutes({...}) and createRouter({ routes: {...} })
|
|
4371
|
+
let match = code.match(/defineRoutes\s*\(\s*\{([^}]+)\}\s*\)/);
|
|
4372
|
+
if (!match) {
|
|
4373
|
+
match = code.match(/routes\s*:\s*\{([^}]+)\}/);
|
|
4374
|
+
}
|
|
4375
|
+
if (!match) return [];
|
|
4376
|
+
|
|
4377
|
+
const paths = [];
|
|
4378
|
+
const entries = match[1].matchAll(/"([^"]+)"\s*:/g);
|
|
4379
|
+
for (const entry of entries) {
|
|
4380
|
+
const path = entry[1];
|
|
4381
|
+
if (path === '404' || path === '*') continue;
|
|
4382
|
+
if (path.includes(':')) continue;
|
|
4383
|
+
paths.push(path);
|
|
4384
|
+
}
|
|
4385
|
+
return paths;
|
|
4386
|
+
}
|
|
4387
|
+
|
|
3451
4388
|
// Fallback JS minifier — string/regex-aware, no AST required
|
|
3452
4389
|
function _simpleMinify(code) {
|
|
3453
4390
|
// Phase 1: Strip comments while respecting strings and regexes
|
|
@@ -3921,6 +4858,10 @@ function collectExports(ast, filename) {
|
|
|
3921
4858
|
allNames.add(node.name);
|
|
3922
4859
|
if (node.isPublic) publicExports.add(node.name);
|
|
3923
4860
|
}
|
|
4861
|
+
if (node.type === 'ComponentDeclaration') {
|
|
4862
|
+
allNames.add(node.name);
|
|
4863
|
+
if (node.isPublic) publicExports.add(node.name);
|
|
4864
|
+
}
|
|
3924
4865
|
if (node.type === 'ImplDeclaration') { /* impl doesn't export a name */ }
|
|
3925
4866
|
}
|
|
3926
4867
|
|
|
@@ -3962,8 +4903,24 @@ function compileWithImports(source, filename, srcDir) {
|
|
|
3962
4903
|
// Collect this module's exports for validation
|
|
3963
4904
|
collectExports(ast, filename);
|
|
3964
4905
|
|
|
3965
|
-
// Resolve .tova
|
|
4906
|
+
// Resolve imports: tova: prefix, @/ prefix, then .tova files
|
|
3966
4907
|
for (const node of ast.body) {
|
|
4908
|
+
// Resolve tova: prefix imports to runtime modules
|
|
4909
|
+
if ((node.type === 'ImportDeclaration' || node.type === 'ImportDefault' || node.type === 'ImportWildcard') && node.source.startsWith('tova:')) {
|
|
4910
|
+
node.source = './runtime/' + node.source.slice(5) + '.js';
|
|
4911
|
+
continue;
|
|
4912
|
+
}
|
|
4913
|
+
// Resolve @/ prefix imports to project root
|
|
4914
|
+
if ((node.type === 'ImportDeclaration' || node.type === 'ImportDefault' || node.type === 'ImportWildcard') && node.source.startsWith('@/')) {
|
|
4915
|
+
const relPath = node.source.slice(2);
|
|
4916
|
+
let resolved = resolve(srcDir, relPath);
|
|
4917
|
+
if (!resolved.endsWith('.tova')) resolved += '.tova';
|
|
4918
|
+
const fromDir = dirname(filename);
|
|
4919
|
+
let rel = relative(fromDir, resolved);
|
|
4920
|
+
if (!rel.startsWith('.')) rel = './' + rel;
|
|
4921
|
+
node.source = rel;
|
|
4922
|
+
// Fall through to .tova import handling below
|
|
4923
|
+
}
|
|
3967
4924
|
if (node.type === 'ImportDeclaration' && node.source.endsWith('.tova')) {
|
|
3968
4925
|
const importPath = resolve(dirname(filename), node.source);
|
|
3969
4926
|
trackDependency(filename, importPath);
|
|
@@ -4180,8 +5137,22 @@ function mergeDirectory(dir, srcDir, options = {}) {
|
|
|
4180
5137
|
// Collect exports for cross-file import validation
|
|
4181
5138
|
collectExports(ast, file);
|
|
4182
5139
|
|
|
4183
|
-
// Resolve
|
|
5140
|
+
// Resolve imports: tova: prefix, @/ prefix, then cross-directory .tova
|
|
4184
5141
|
for (const node of ast.body) {
|
|
5142
|
+
if ((node.type === 'ImportDeclaration' || node.type === 'ImportDefault' || node.type === 'ImportWildcard') && node.source.startsWith('tova:')) {
|
|
5143
|
+
node.source = './runtime/' + node.source.slice(5) + '.js';
|
|
5144
|
+
continue;
|
|
5145
|
+
}
|
|
5146
|
+
if ((node.type === 'ImportDeclaration' || node.type === 'ImportDefault' || node.type === 'ImportWildcard') && node.source.startsWith('@/')) {
|
|
5147
|
+
const relPath = node.source.slice(2);
|
|
5148
|
+
let resolved = resolve(srcDir, relPath);
|
|
5149
|
+
if (!resolved.endsWith('.tova')) resolved += '.tova';
|
|
5150
|
+
const fromDir = dirname(file);
|
|
5151
|
+
let rel = relative(fromDir, resolved);
|
|
5152
|
+
if (!rel.startsWith('.')) rel = './' + rel;
|
|
5153
|
+
node.source = rel;
|
|
5154
|
+
// Fall through to .tova import handling below
|
|
5155
|
+
}
|
|
4185
5156
|
if ((node.type === 'ImportDeclaration' || node.type === 'ImportDefault' || node.type === 'ImportWildcard') && node.source.endsWith('.tova')) {
|
|
4186
5157
|
const importPath = resolve(dirname(file), node.source);
|
|
4187
5158
|
// Only process imports from OTHER directories (same-dir files are merged)
|
|
@@ -4257,7 +5228,7 @@ function mergeDirectory(dir, srcDir, options = {}) {
|
|
|
4257
5228
|
const mergedAST = new Program(mergedBody);
|
|
4258
5229
|
|
|
4259
5230
|
// Run analyzer on merged AST
|
|
4260
|
-
const analyzer = new Analyzer(mergedAST, dir);
|
|
5231
|
+
const analyzer = new Analyzer(mergedAST, dir, { strict: options.strict, strictSecurity: options.strictSecurity });
|
|
4261
5232
|
const { warnings } = analyzer.analyze();
|
|
4262
5233
|
|
|
4263
5234
|
if (warnings.length > 0) {
|
|
@@ -4278,7 +5249,27 @@ function mergeDirectory(dir, srcDir, options = {}) {
|
|
|
4278
5249
|
output._sourceContents = sourceContents;
|
|
4279
5250
|
output._sourceFiles = tovaFiles;
|
|
4280
5251
|
|
|
4281
|
-
|
|
5252
|
+
// Extract security info for scorecard
|
|
5253
|
+
const hasServer = mergedBody.some(n => n.type === 'ServerBlock');
|
|
5254
|
+
const hasEdge = mergedBody.some(n => n.type === 'EdgeBlock');
|
|
5255
|
+
const securityNode = mergedBody.find(n => n.type === 'SecurityBlock');
|
|
5256
|
+
let securityConfig = null;
|
|
5257
|
+
if (securityNode) {
|
|
5258
|
+
securityConfig = {};
|
|
5259
|
+
for (const child of securityNode.body || []) {
|
|
5260
|
+
if (child.type === 'AuthDeclaration') securityConfig.auth = { authType: child.authType || 'jwt', storage: child.config?.storage?.value };
|
|
5261
|
+
else if (child.type === 'CsrfDeclaration') securityConfig.csrf = { enabled: child.config?.enabled?.value !== false };
|
|
5262
|
+
else if (child.type === 'RateLimitDeclaration') securityConfig.rateLimit = { max: child.config?.max?.value };
|
|
5263
|
+
else if (child.type === 'CspDeclaration') securityConfig.csp = { default_src: true };
|
|
5264
|
+
else if (child.type === 'CorsDeclaration') {
|
|
5265
|
+
const origins = child.config?.origins;
|
|
5266
|
+
securityConfig.cors = { origins: origins ? (origins.elements || []).map(e => e.value) : [] };
|
|
5267
|
+
}
|
|
5268
|
+
else if (child.type === 'AuditDeclaration') securityConfig.audit = { events: ['auth'] };
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
|
|
5272
|
+
return { output, files: tovaFiles, single: false, warnings, securityConfig, hasServer, hasEdge };
|
|
4282
5273
|
}
|
|
4283
5274
|
|
|
4284
5275
|
// Group .tova files by their parent directory
|
|
@@ -4451,7 +5442,7 @@ function completionsCommand(shell) {
|
|
|
4451
5442
|
'migrate:create', 'migrate:up', 'migrate:down', 'migrate:reset', 'migrate:fresh', 'migrate:status',
|
|
4452
5443
|
];
|
|
4453
5444
|
|
|
4454
|
-
const globalFlags = ['--help', '--version', '--output', '--production', '--watch', '--verbose', '--quiet', '--debug', '--strict'];
|
|
5445
|
+
const globalFlags = ['--help', '--version', '--output', '--production', '--watch', '--verbose', '--quiet', '--debug', '--strict', '--strict-security'];
|
|
4455
5446
|
|
|
4456
5447
|
switch (shell) {
|
|
4457
5448
|
case 'bash': {
|
|
@@ -4474,7 +5465,7 @@ _tova() {
|
|
|
4474
5465
|
return 0
|
|
4475
5466
|
;;
|
|
4476
5467
|
--template)
|
|
4477
|
-
COMPREPLY=( $(compgen -W "fullstack api script library blank" -- "\${cur}") )
|
|
5468
|
+
COMPREPLY=( $(compgen -W "fullstack spa site api script library blank" -- "\${cur}") )
|
|
4478
5469
|
return 0
|
|
4479
5470
|
;;
|
|
4480
5471
|
completions)
|
|
@@ -4522,7 +5513,7 @@ ${commands.map(c => ` '${c}:${c} command'`).join('\n')}
|
|
|
4522
5513
|
case $words[1] in
|
|
4523
5514
|
new)
|
|
4524
5515
|
_arguments \\
|
|
4525
|
-
'--template[Project template]:template:(fullstack api script library blank)' \\
|
|
5516
|
+
'--template[Project template]:template:(fullstack spa site api script library blank)' \\
|
|
4526
5517
|
'*:name:'
|
|
4527
5518
|
;;
|
|
4528
5519
|
run|build|check|fmt|doc)
|
|
@@ -4614,7 +5605,7 @@ _tova "$@"
|
|
|
4614
5605
|
script += `complete -c tova -l debug -d 'Debug output'\n`;
|
|
4615
5606
|
script += `complete -c tova -l strict -d 'Strict type checking'\n`;
|
|
4616
5607
|
script += `\n# Template completions for 'new'\n`;
|
|
4617
|
-
script += `complete -c tova -n '__fish_seen_subcommand_from new' -l template -d 'Project template' -xa 'fullstack api script library blank'\n`;
|
|
5608
|
+
script += `complete -c tova -n '__fish_seen_subcommand_from new' -l template -d 'Project template' -xa 'fullstack spa site api script library blank'\n`;
|
|
4618
5609
|
script += `\n# Shell completions for 'completions'\n`;
|
|
4619
5610
|
script += `complete -c tova -n '__fish_seen_subcommand_from completions' -xa 'bash zsh fish'\n`;
|
|
4620
5611
|
|