tw-framework 0.2.3__tar.gz → 0.2.4__tar.gz
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.
- tw_framework-0.2.4/PKG-INFO +101 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/pyproject.toml +3 -2
- tw_framework-0.2.4/tw_framework/adapters/__init__.py +3 -0
- tw_framework-0.2.4/tw_framework/adapters/cloudflare.py +96 -0
- tw_framework-0.2.4/tw_framework/adapters/netlify.py +91 -0
- tw_framework-0.2.4/tw_framework/adapters/vercel.py +192 -0
- tw_framework-0.2.4/tw_framework/adapters/vercel_functions.py +554 -0
- tw_framework-0.2.4/tw_framework.egg-info/PKG-INFO +101 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework.egg-info/SOURCES.txt +6 -1
- tw_framework-0.2.3/PKG-INFO +0 -8
- tw_framework-0.2.3/tw_framework.egg-info/PKG-INFO +0 -8
- {tw_framework-0.2.3 → tw_framework-0.2.4}/README.md +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/setup.cfg +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tests/test_deployment.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/__init__.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/advanced_diagnostics.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/asset_optimizer.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/ast_nodes.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/build.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/build_performance.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/build_report.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/cli.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/code_splitting.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/common.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/compiler.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/compiler_stats.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/dead_code.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/dependency_graph.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/deploy.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/dev.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/diagnostics.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/dynamic_imports.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/error_formatter.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/framework.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/hydration.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/incremental_cache.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/interpreter.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/ir.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/lexer.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/lowering.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/parser.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/partial_rebuild.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/performance_analyzer.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/plugin_runtime.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/production_optimizer.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/reactivity.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/render_css.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/render_html.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/route_optimizer.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/router.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/runtime_model.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/runtime_values.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/semantic.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/server.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/signature.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/static_dynamic_auto.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/streaming.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/test_build_cli_pytest.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/tree_shaking.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework/twm_parser.py +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework.egg-info/dependency_links.txt +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework.egg-info/entry_points.txt +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework.egg-info/requires.txt +0 -0
- {tw_framework-0.2.3 → tw_framework-0.2.4}/tw_framework.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tw-framework
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: TW framework CLI with create, dev, build, and deploy commands
|
|
5
|
+
Author: TW Framework
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
10
|
+
|
|
11
|
+
# TW Framework
|
|
12
|
+
|
|
13
|
+
TW is a custom language + framework for building websites with a Next.js‑like developer experience.
|
|
14
|
+
|
|
15
|
+
## Zero‑Config Deployment
|
|
16
|
+
|
|
17
|
+
1. Create a GitHub repository with your TW project (containing `tw.config`, `pages/`, `components/`, `public/`).
|
|
18
|
+
2. Connect the repository to Vercel or Netlify.
|
|
19
|
+
3. The platform automatically detects `tw.config` and runs `tw build`.
|
|
20
|
+
4. Your site goes live – no terminal commands required.
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Create a new project
|
|
26
|
+
tw init my-site
|
|
27
|
+
cd my-site
|
|
28
|
+
|
|
29
|
+
# Development server
|
|
30
|
+
tw dev
|
|
31
|
+
|
|
32
|
+
# Production build
|
|
33
|
+
tw build
|
|
34
|
+
|
|
35
|
+
# Preview the build
|
|
36
|
+
tw preview
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## CLI Commands
|
|
40
|
+
|
|
41
|
+
| Command | Description |
|
|
42
|
+
|---------|-------------|
|
|
43
|
+
| `tw init [name]` | Create a new TW project in the current directory |
|
|
44
|
+
| `tw dev` | Start the development server with live reload |
|
|
45
|
+
| `tw build` | Generate a production build |
|
|
46
|
+
| `tw preview` | Preview the built output |
|
|
47
|
+
| `tw export` | Generate a static export |
|
|
48
|
+
| `tw deploy` | Build and deploy to a hosting provider |
|
|
49
|
+
| `tw doctor` | Run project health checks and deployment compatibility |
|
|
50
|
+
| `tw ast file.tw` | Print the AST JSON for a TW source file |
|
|
51
|
+
| `tw ir file.tw` | Print the IR JSON for a TW source file |
|
|
52
|
+
| `tw run file.tw` | Interpret a TW file and output HTML |
|
|
53
|
+
| `tw check file.tw` | Print diagnostics for a TW file |
|
|
54
|
+
| `tw tokens file.tw` | Print the token stream JSON for a TW source file |
|
|
55
|
+
| `tw clean` | Clean dist and hidden cache folders |
|
|
56
|
+
| `tw info` | Show a project summary |
|
|
57
|
+
| `tw login` | Save deploy provider configuration |
|
|
58
|
+
|
|
59
|
+
## Deployment Metadata
|
|
60
|
+
|
|
61
|
+
After a build, the file `dist/tw.deploy.json` is automatically generated. It contains:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"framework": "tw",
|
|
66
|
+
"version": "1.0.0",
|
|
67
|
+
"build": "tw build",
|
|
68
|
+
"output": "dist",
|
|
69
|
+
"runtime": "ssr"
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Deployment platforms can read this file to detect the framework and configure the build.
|
|
74
|
+
|
|
75
|
+
## Adapters
|
|
76
|
+
|
|
77
|
+
TW Framework includes built‑in adapters for Vercel and Netlify. They provide:
|
|
78
|
+
|
|
79
|
+
- Framework detection (`detect()` function)
|
|
80
|
+
- Automatic generation of `vercel.json` / `netlify.toml` when missing
|
|
81
|
+
- Build command, output directory, install command, and SSR handling
|
|
82
|
+
|
|
83
|
+
## Documentation
|
|
84
|
+
|
|
85
|
+
- [Getting Started](docs/getting-started.md)
|
|
86
|
+
- [Deployment to Vercel](docs/deployment/vercel.md)
|
|
87
|
+
- [Deployment to Netlify](docs/deployment/netlify.md)
|
|
88
|
+
|
|
89
|
+
## Testing
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python3 -m unittest discover -s tests -q
|
|
93
|
+
python3 run_tests.py
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Debug Flags
|
|
97
|
+
|
|
98
|
+
- `TW_WARN_LITERAL_PARSE=1` – warn when literal parsing falls back to string
|
|
99
|
+
- `TW_STRICT_EVAL=1` – re‑raise runtime expression errors instead of warning
|
|
100
|
+
|
|
101
|
+
These flags are mainly useful while debugging parser/runtime edge cases or tightening CI checks around expression handling.
|
|
@@ -4,8 +4,9 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tw-framework"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.4"
|
|
8
8
|
description = "TW framework CLI with create, dev, build, and deploy commands"
|
|
9
|
+
readme = "README.md"
|
|
9
10
|
requires-python = ">=3.9"
|
|
10
11
|
authors = [
|
|
11
12
|
{ name = "TW Framework" }
|
|
@@ -22,4 +23,4 @@ dev = [
|
|
|
22
23
|
tw = "tw_framework.cli:main"
|
|
23
24
|
|
|
24
25
|
[tool.setuptools]
|
|
25
|
-
packages = ["tw_framework"]
|
|
26
|
+
packages = ["tw_framework", "tw_framework.adapters"]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cloudflare adapter for TWLang framework.
|
|
3
|
+
Generates _routes.json and _worker.js for Cloudflare Pages.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, Any
|
|
9
|
+
|
|
10
|
+
def generate_cloudflare_output(
|
|
11
|
+
dist_dir: str,
|
|
12
|
+
config: Dict[str, Any],
|
|
13
|
+
project_root: str,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Generate Cloudflare deployment files from build output.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
dist_dir: Path to the build output directory (e.g., 'dist').
|
|
20
|
+
config: JSON to tw.config as a dictionary.
|
|
21
|
+
project_root: Root directory of the project.
|
|
22
|
+
"""
|
|
23
|
+
root = Path(project_root)
|
|
24
|
+
routes_path = root / "_routes.json"
|
|
25
|
+
worker_path = root / "_worker.js"
|
|
26
|
+
|
|
27
|
+
# Build _routes.json
|
|
28
|
+
routes = []
|
|
29
|
+
# Add redirects
|
|
30
|
+
redirects = config.get("redirects", {}).get("rule", [])
|
|
31
|
+
for rule in redirects:
|
|
32
|
+
source = rule.get("source", "")
|
|
33
|
+
destination = rule.get("destination", "")
|
|
34
|
+
status = 301 if rule.get("permanent") else 302
|
|
35
|
+
routes.append({
|
|
36
|
+
"src": source,
|
|
37
|
+
"dest": destination,
|
|
38
|
+
"status": status,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
# Add rewrites
|
|
42
|
+
rewrites = config.get("rewrites", {}).get("rule", [])
|
|
43
|
+
for rule in rewrites:
|
|
44
|
+
source = rule.get("source", "")
|
|
45
|
+
destination = rule.get("destination", "")
|
|
46
|
+
routes.append({
|
|
47
|
+
"src": source,
|
|
48
|
+
"dest": destination,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
# Add headers
|
|
52
|
+
headers = config.get("headers", {}).get("rule", [])
|
|
53
|
+
for rule in headers:
|
|
54
|
+
source = rule.get("source", "/**")
|
|
55
|
+
sets = rule.get("set", {})
|
|
56
|
+
headers_dict = {}
|
|
57
|
+
for h_name, h_value in sets.items():
|
|
58
|
+
headers_dict[h_name] = h_value
|
|
59
|
+
routes.append({
|
|
60
|
+
"src": source,
|
|
61
|
+
"headers": headers_dict,
|
|
62
|
+
"continue": True,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
# Static assets catch-all
|
|
66
|
+
routes.append({
|
|
67
|
+
"src": "/(.*)",
|
|
68
|
+
"dest": "/$1",
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
with open(routes_path, "w") as f:
|
|
72
|
+
json.dump({"version": 1, "routes": routes}, f, indent=2)
|
|
73
|
+
|
|
74
|
+
# Generate _worker.js for API routes
|
|
75
|
+
dist_path = Path(dist_dir)
|
|
76
|
+
functions_src = dist_path / "functions"
|
|
77
|
+
if functions_src.exists():
|
|
78
|
+
worker_code = """
|
|
79
|
+
export default {
|
|
80
|
+
async fetch(request, env, ctx) {
|
|
81
|
+
const url = new URL(request.url);
|
|
82
|
+
// Handle API routes
|
|
83
|
+
if (url.pathname.startsWith('/api/')) {
|
|
84
|
+
return new Response(JSON.stringify({ message: "API route" }), {
|
|
85
|
+
headers: { "Content-Type": "application/json" }
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// Otherwise serve static assets from KV or R2
|
|
89
|
+
return env.ASSETS.fetch(request);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
"""
|
|
93
|
+
with open(worker_path, "w") as f:
|
|
94
|
+
f.write(worker_code)
|
|
95
|
+
|
|
96
|
+
print(f"Generated Cloudflare deployment files at {root}")
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Netlify adapter for TWLang framework.
|
|
3
|
+
Generates netlify.toml and functions directory.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, Any
|
|
9
|
+
|
|
10
|
+
def detect() -> dict:
|
|
11
|
+
"""Return framework metadata for Netlify detection."""
|
|
12
|
+
return {
|
|
13
|
+
"framework": "tw",
|
|
14
|
+
"version": "1.0",
|
|
15
|
+
"buildCommand": "tw build",
|
|
16
|
+
"publish": "dist",
|
|
17
|
+
"installCommand": "pip install tw-framework",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate_netlify_output(
|
|
22
|
+
dist_dir: str,
|
|
23
|
+
config: Dict[str, Any],
|
|
24
|
+
project_root: str,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Generate Netlify deployment files from build output.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
dist_dir: Path to the build output directory (e.g., 'dist').
|
|
31
|
+
config: Parsed tw.config as a dictionary.
|
|
32
|
+
project_root: Root directory of the project.
|
|
33
|
+
"""
|
|
34
|
+
root = Path(project_root)
|
|
35
|
+
netlify_toml_path = root / "netlify.toml"
|
|
36
|
+
functions_dir = root / "netlify" / "functions"
|
|
37
|
+
publish_dir = "dist"
|
|
38
|
+
|
|
39
|
+
# Build netlify.toml
|
|
40
|
+
toml_lines = ['[build]', 'command = "tw build --prod"', f'publish = "{publish_dir}"', '']
|
|
41
|
+
|
|
42
|
+
# Add redirects from tw.config
|
|
43
|
+
redirects = config.get("redirects", {}).get("rule", [])
|
|
44
|
+
if redirects:
|
|
45
|
+
toml_lines.append('[[redirects]]')
|
|
46
|
+
for rule in redirects:
|
|
47
|
+
source = rule.get("source", "")
|
|
48
|
+
destination = rule.get("destination", "")
|
|
49
|
+
status = 301 if rule.get("permanent") else 302
|
|
50
|
+
toml_lines.append(f' from = "{source}"')
|
|
51
|
+
toml_lines.append(f' to = "{destination}"')
|
|
52
|
+
toml_lines.append(f' status = {status}')
|
|
53
|
+
toml_lines.append('')
|
|
54
|
+
|
|
55
|
+
# Add headers from tw.config
|
|
56
|
+
headers = config.get("headers", {}).get("rule", [])
|
|
57
|
+
if headers:
|
|
58
|
+
toml_lines.append('[[headers]]')
|
|
59
|
+
for rule in headers:
|
|
60
|
+
source = rule.get("source", "/**")
|
|
61
|
+
sets = rule.get("set", {})
|
|
62
|
+
for h_name, h_value in sets.items():
|
|
63
|
+
toml_lines.append(f' for = "{source}"')
|
|
64
|
+
toml_lines.append(' [headers.values]')
|
|
65
|
+
toml_lines.append(f' {h_name} = "{h_value}"')
|
|
66
|
+
toml_lines.append('')
|
|
67
|
+
|
|
68
|
+
with open(netlify_toml_path, "w") as f:
|
|
69
|
+
f.write("\n".join(toml_lines))
|
|
70
|
+
|
|
71
|
+
# Copy API functions if present
|
|
72
|
+
dist_path = Path(dist_dir)
|
|
73
|
+
functions_src = dist_path / "functions"
|
|
74
|
+
if functions_src.exists():
|
|
75
|
+
if functions_dir.exists():
|
|
76
|
+
shutil.rmtree(functions_dir)
|
|
77
|
+
shutil.copytree(functions_src, functions_dir)
|
|
78
|
+
|
|
79
|
+
# Auto‑generate netlify.toml for zero‑config detection
|
|
80
|
+
netlify_toml_path = root / "netlify.toml"
|
|
81
|
+
if not netlify_toml_path.exists():
|
|
82
|
+
toml_content = (
|
|
83
|
+
"[build]\n"
|
|
84
|
+
'command = "tw build --prod"\n'
|
|
85
|
+
'publish = "dist"\n'
|
|
86
|
+
)
|
|
87
|
+
with open(netlify_toml_path, "w") as f:
|
|
88
|
+
f.write(toml_content)
|
|
89
|
+
print(f" ✅ Auto‑generated netlify.toml at {netlify_toml_path}")
|
|
90
|
+
|
|
91
|
+
print(f"Generated Netlify deployment files at {root}")
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Vercel adapter for TWLang framework.
|
|
3
|
+
Generates .vercel/output compliant with Build Output API v3.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any
|
|
10
|
+
|
|
11
|
+
def detect() -> dict:
|
|
12
|
+
"""Return framework metadata for Vercel detection."""
|
|
13
|
+
return {
|
|
14
|
+
"framework": "tw",
|
|
15
|
+
"version": "1.0",
|
|
16
|
+
"buildCommand": "tw build",
|
|
17
|
+
"outputDirectory": "dist",
|
|
18
|
+
"installCommand": "pip install tw-framework",
|
|
19
|
+
"devCommand": "tw dev",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def generate_vercel_output(
|
|
24
|
+
dist_dir: str,
|
|
25
|
+
config: Dict[str, Any],
|
|
26
|
+
project_root: str,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Generate .vercel/output from the build output directory.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
dist_dir: Path to the build output directory (e.g., 'dist').
|
|
33
|
+
config: Parsed tw.config as a dictionary.
|
|
34
|
+
project_root: Root directory of the project.
|
|
35
|
+
"""
|
|
36
|
+
output_dir = Path(project_root) / ".vercel" / "output"
|
|
37
|
+
# Clean previous output
|
|
38
|
+
if output_dir.exists():
|
|
39
|
+
shutil.rmtree(output_dir)
|
|
40
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
static_dir = output_dir / "static"
|
|
43
|
+
static_dir.mkdir(exist_ok=True)
|
|
44
|
+
|
|
45
|
+
# Copy all files from dist to static (except functions/api)
|
|
46
|
+
dist_path = Path(dist_dir)
|
|
47
|
+
if not dist_path.exists():
|
|
48
|
+
raise FileNotFoundError(f"Build output directory '{dist_dir}' not found")
|
|
49
|
+
|
|
50
|
+
functions_src = dist_path / "functions"
|
|
51
|
+
functions_dst = output_dir / "functions"
|
|
52
|
+
|
|
53
|
+
for item in dist_path.rglob("*"):
|
|
54
|
+
if item.is_file():
|
|
55
|
+
rel = item.relative_to(dist_path)
|
|
56
|
+
# Skip functions directory (handled separately)
|
|
57
|
+
if rel.parts and rel.parts[0] == "functions":
|
|
58
|
+
continue
|
|
59
|
+
dest = static_dir / rel
|
|
60
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
shutil.copy2(item, dest)
|
|
62
|
+
|
|
63
|
+
# Handle functions if present
|
|
64
|
+
if functions_src.exists():
|
|
65
|
+
if functions_dst.exists():
|
|
66
|
+
shutil.rmtree(functions_dst)
|
|
67
|
+
shutil.copytree(functions_src, functions_dst)
|
|
68
|
+
|
|
69
|
+
# Build config.json
|
|
70
|
+
vercel_config = build_vercel_config(config, dist_path)
|
|
71
|
+
config_path = output_dir / "config.json"
|
|
72
|
+
with open(config_path, "w") as f:
|
|
73
|
+
json.dump(vercel_config, f, indent=2)
|
|
74
|
+
|
|
75
|
+
# Auto‑generate vercel.json for zero‑config detection
|
|
76
|
+
vercel_json_path = Path(project_root) / "vercel.json"
|
|
77
|
+
if not vercel_json_path.exists():
|
|
78
|
+
vercel_config = {
|
|
79
|
+
"framework": "tw",
|
|
80
|
+
"buildCommand": "tw build",
|
|
81
|
+
"outputDirectory": "dist",
|
|
82
|
+
"installCommand": "pip install tw-framework",
|
|
83
|
+
}
|
|
84
|
+
with open(vercel_json_path, "w") as f:
|
|
85
|
+
json.dump(vercel_config, f, indent=2)
|
|
86
|
+
print(f" ✅ Auto‑generated vercel.json at {vercel_json_path}")
|
|
87
|
+
|
|
88
|
+
print(f"Generated .vercel/output at {output_dir}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def build_vercel_config(tw_config: Dict[str, Any], dist_path: Path) -> Dict[str, Any]:
|
|
92
|
+
"""
|
|
93
|
+
Build the Vercel config.json (version 3) from tw.config and build output.
|
|
94
|
+
"""
|
|
95
|
+
routes = []
|
|
96
|
+
headers_list = []
|
|
97
|
+
rewrites_list = []
|
|
98
|
+
redirects_list = []
|
|
99
|
+
|
|
100
|
+
# Extract headers from tw.config
|
|
101
|
+
headers_cfg = tw_config.get("headers", {}).get("rule", [])
|
|
102
|
+
for rule in headers_cfg:
|
|
103
|
+
source = rule.get("source", "/**")
|
|
104
|
+
headers = {}
|
|
105
|
+
sets = rule.get("set", {})
|
|
106
|
+
for h_name, h_value in sets.items():
|
|
107
|
+
headers[h_name] = h_value
|
|
108
|
+
if headers:
|
|
109
|
+
headers_list.append({
|
|
110
|
+
"source": source,
|
|
111
|
+
"headers": [{"key": k, "value": v} for k, v in headers.items()]
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
# Extract redirects
|
|
115
|
+
redirects_cfg = tw_config.get("redirects", {}).get("rule", [])
|
|
116
|
+
for rule in redirects_cfg:
|
|
117
|
+
source = rule.get("source", "")
|
|
118
|
+
destination = rule.get("destination", "")
|
|
119
|
+
permanent = rule.get("permanent", False)
|
|
120
|
+
redirects_list.append({
|
|
121
|
+
"source": source,
|
|
122
|
+
"destination": destination,
|
|
123
|
+
"permanent": permanent,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
# Extract rewrites
|
|
127
|
+
rewrites_cfg = tw_config.get("rewrites", {}).get("rule", [])
|
|
128
|
+
for rule in rewrites_cfg:
|
|
129
|
+
source = rule.get("source", "")
|
|
130
|
+
destination = rule.get("destination", "")
|
|
131
|
+
rewrites_list.append({
|
|
132
|
+
"source": source,
|
|
133
|
+
"destination": destination,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
# Build routes array
|
|
137
|
+
# 1. Handle redirects first
|
|
138
|
+
for r in redirects_list:
|
|
139
|
+
routes.append({
|
|
140
|
+
"src": r["source"],
|
|
141
|
+
"status": 308 if r.get("permanent") else 307,
|
|
142
|
+
"headers": {"Location": r["destination"]},
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
# 2. Handle rewrites
|
|
146
|
+
for r in rewrites_list:
|
|
147
|
+
routes.append({
|
|
148
|
+
"src": r["source"],
|
|
149
|
+
"dest": r["destination"],
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
# 3. Apply headers
|
|
153
|
+
for h in headers_list:
|
|
154
|
+
routes.append({
|
|
155
|
+
"src": h["source"],
|
|
156
|
+
"headers": h["headers"],
|
|
157
|
+
"continue": True,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
# 4. Static files and functions
|
|
161
|
+
has_functions = (dist_path / "functions").exists()
|
|
162
|
+
if has_functions:
|
|
163
|
+
routes.append({
|
|
164
|
+
"src": "/api/(.*)",
|
|
165
|
+
"dest": "/api/$1",
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
# 5. Static files catch-all
|
|
169
|
+
routes.append({
|
|
170
|
+
"src": "/(.*)",
|
|
171
|
+
"dest": "/$1",
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
config = {
|
|
175
|
+
"version": 3,
|
|
176
|
+
"routes": routes,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# Add images config if present
|
|
180
|
+
images_cfg = tw_config.get("images", {})
|
|
181
|
+
if images_cfg:
|
|
182
|
+
config["images"] = {
|
|
183
|
+
"sizes": images_cfg.get("sizes", [640, 750, 828, 1080, 1200, 1920, 2048, 3840]),
|
|
184
|
+
"domains": [],
|
|
185
|
+
"remotePatterns": images_cfg.get("remote_patterns", []),
|
|
186
|
+
"minimumCacheTTL": 60,
|
|
187
|
+
"formats": ["image/avif", "image/webp"],
|
|
188
|
+
"dangerouslyAllowSVG": False,
|
|
189
|
+
"contentSecurityPolicy": "script-src 'none'; frame-src 'none'; sandbox;",
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return config
|