itw-python-builder 0.1.30__tar.gz → 0.1.32__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.
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/PKG-INFO +1 -1
- itw_python_builder-0.1.32/itw_python_builder/ssr_tasks.py +348 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder.egg-info/PKG-INFO +1 -1
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/pyproject.toml +1 -1
- itw_python_builder-0.1.30/itw_python_builder/ssr_tasks.py +0 -210
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/LICENSE +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/README.md +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder/.pylintrc +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder/__init__.py +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder/cli.py +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder/tasks.py +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder/templates/server.sitemap.snippet.ts +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder/templates/sitemap.routes.ts +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder/utils.py +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder/version.py +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder.egg-info/SOURCES.txt +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder.egg-info/dependency_links.txt +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder.egg-info/entry_points.txt +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder.egg-info/requires.txt +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder.egg-info/top_level.txt +0 -0
- {itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/setup.cfg +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""SSR scaffolding tasks. One-time `itw ssr-init` for Angular SSR + ngx-seo-helper.
|
|
2
|
+
|
|
3
|
+
The build/version tasks (`incrementrc`, `release`, etc.) live in tasks.py and
|
|
4
|
+
take an `ssr=False` kwarg. With --ssr they invoke:
|
|
5
|
+
- `npm run build:ssr` on master
|
|
6
|
+
- `npm run build:ssr:staging` on any other branch
|
|
7
|
+
|
|
8
|
+
These two scripts are a contract the project owner defines in package.json.
|
|
9
|
+
ssr-init intentionally does NOT write them, because the right command depends
|
|
10
|
+
on which Angular SSR setup the project ended up with:
|
|
11
|
+
|
|
12
|
+
legacy browser builder → ng build --configuration=X && ng run <name>:server:X
|
|
13
|
+
modern application builder (Angular 17+, fresh `ng add @angular/ssr`)
|
|
14
|
+
→ ng build --configuration=X
|
|
15
|
+
"""
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from invoke import task, Context
|
|
22
|
+
|
|
23
|
+
from itw_python_builder.utils import detect_project_type, _read_package_name, is_ssr_project
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _angular_major_version() -> int:
|
|
27
|
+
"""Read @angular/core's major version from package.json. Returns 0 if not found."""
|
|
28
|
+
pkg_path = os.path.join(os.getcwd(), 'package.json')
|
|
29
|
+
with open(pkg_path, 'r', encoding='utf-8') as fp:
|
|
30
|
+
data = json.load(fp)
|
|
31
|
+
deps = {**data.get('dependencies', {}), **data.get('devDependencies', {})}
|
|
32
|
+
spec = deps.get('@angular/core', '')
|
|
33
|
+
match = re.match(r'[\^~>=<]*(\d+)', spec)
|
|
34
|
+
return int(match.group(1)) if match else 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _angular_core_installed_version() -> str:
|
|
38
|
+
"""Return the exact installed @angular/core version from node_modules, or '' if not found."""
|
|
39
|
+
pkg_path = os.path.join(os.getcwd(), 'node_modules', '@angular', 'core', 'package.json')
|
|
40
|
+
if not os.path.isfile(pkg_path):
|
|
41
|
+
return ''
|
|
42
|
+
with open(pkg_path, 'r', encoding='utf-8') as fp:
|
|
43
|
+
return json.load(fp).get('version', '')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _read_angular_json() -> dict:
|
|
47
|
+
"""Read angular.json from cwd. Returns empty dict if missing/invalid."""
|
|
48
|
+
path = os.path.join(os.getcwd(), 'angular.json')
|
|
49
|
+
if not os.path.isfile(path):
|
|
50
|
+
return {}
|
|
51
|
+
try:
|
|
52
|
+
with open(path, 'r', encoding='utf-8') as fp:
|
|
53
|
+
return json.load(fp)
|
|
54
|
+
except (OSError, json.JSONDecodeError):
|
|
55
|
+
return {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _detect_angular_project(angular_json: dict) -> tuple:
|
|
59
|
+
"""Return (project_name, project_data) for the project SSR was scaffolded into.
|
|
60
|
+
|
|
61
|
+
Priority: package.json `name` match → defaultProject → only project in repo.
|
|
62
|
+
Returns (None, None) if angular.json has no usable projects.
|
|
63
|
+
"""
|
|
64
|
+
projects = angular_json.get('projects') or {}
|
|
65
|
+
if not projects:
|
|
66
|
+
return (None, None)
|
|
67
|
+
try:
|
|
68
|
+
pkg_name = _read_package_name()
|
|
69
|
+
if pkg_name in projects:
|
|
70
|
+
return (pkg_name, projects[pkg_name])
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
default = angular_json.get('defaultProject')
|
|
74
|
+
if default and default in projects:
|
|
75
|
+
return (default, projects[default])
|
|
76
|
+
if len(projects) == 1:
|
|
77
|
+
name = next(iter(projects))
|
|
78
|
+
return (name, projects[name])
|
|
79
|
+
return (None, None)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _maybe_add_ssr_scripts(ctx: Context) -> None:
|
|
83
|
+
"""Add build:ssr, build:ssr:staging, build:ssr:dev npm scripts based on angular.json.
|
|
84
|
+
|
|
85
|
+
Reads angular.json to detect:
|
|
86
|
+
- The builder (legacy `:browser` + separate `server` target, or modern `:application`)
|
|
87
|
+
- The project name (for `ng run <name>:server:<config>` in the legacy form)
|
|
88
|
+
- Which configurations exist (only writes scripts for configs that exist in both
|
|
89
|
+
`build` and, for legacy, `server` targets)
|
|
90
|
+
|
|
91
|
+
Never overwrites a script that already exists in package.json. Prints a summary
|
|
92
|
+
of what was added vs preserved vs skipped (no matching config).
|
|
93
|
+
"""
|
|
94
|
+
angular_json = _read_angular_json()
|
|
95
|
+
if not angular_json:
|
|
96
|
+
print('[itw] No angular.json found — skipping SSR script setup.')
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
project_name, project = _detect_angular_project(angular_json)
|
|
100
|
+
if not project:
|
|
101
|
+
print('[itw] Could not detect Angular project in angular.json — skipping SSR script setup.')
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
arch = project.get('architect') or project.get('targets') or {}
|
|
105
|
+
build = arch.get('build') or {}
|
|
106
|
+
server = arch.get('server') or {}
|
|
107
|
+
builder = build.get('builder', '')
|
|
108
|
+
|
|
109
|
+
is_legacy = builder == '@angular-devkit/build-angular:browser' and bool(server)
|
|
110
|
+
is_modern = builder == '@angular-devkit/build-angular:application'
|
|
111
|
+
if not (is_legacy or is_modern):
|
|
112
|
+
print(f'[itw] Unrecognized build builder {builder!r} — skipping SSR script setup.')
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
build_configs = set((build.get('configurations') or {}).keys())
|
|
116
|
+
if is_legacy:
|
|
117
|
+
server_configs = set((server.get('configurations') or {}).keys())
|
|
118
|
+
usable_configs = build_configs & server_configs
|
|
119
|
+
builder_label = 'legacy (browser + server targets)'
|
|
120
|
+
else:
|
|
121
|
+
usable_configs = build_configs
|
|
122
|
+
builder_label = 'modern (application builder)'
|
|
123
|
+
|
|
124
|
+
print(f"[itw] Detected {builder_label} for project '{project_name}'.")
|
|
125
|
+
|
|
126
|
+
def cmd_for(config: str) -> str:
|
|
127
|
+
if is_legacy:
|
|
128
|
+
return f'ng build --configuration={config} && ng run {project_name}:server:{config}'
|
|
129
|
+
return f'ng build --configuration={config}'
|
|
130
|
+
|
|
131
|
+
script_map = {
|
|
132
|
+
'production': 'build:ssr',
|
|
133
|
+
'staging': 'build:ssr:staging',
|
|
134
|
+
'development': 'build:ssr:dev',
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pkg_path = os.path.join(os.getcwd(), 'package.json')
|
|
138
|
+
with open(pkg_path, 'r', encoding='utf-8') as fp:
|
|
139
|
+
data = json.load(fp)
|
|
140
|
+
scripts = data.setdefault('scripts', {})
|
|
141
|
+
|
|
142
|
+
added, preserved, skipped = [], [], []
|
|
143
|
+
for config_name, script_name in script_map.items():
|
|
144
|
+
if config_name not in usable_configs:
|
|
145
|
+
skipped.append((script_name, config_name))
|
|
146
|
+
continue
|
|
147
|
+
if script_name in scripts:
|
|
148
|
+
preserved.append(script_name)
|
|
149
|
+
continue
|
|
150
|
+
scripts[script_name] = cmd_for(config_name)
|
|
151
|
+
added.append((script_name, scripts[script_name]))
|
|
152
|
+
|
|
153
|
+
if added:
|
|
154
|
+
with open(pkg_path, 'w', encoding='utf-8') as fp:
|
|
155
|
+
json.dump(data, fp, indent=2)
|
|
156
|
+
fp.write('\n')
|
|
157
|
+
|
|
158
|
+
print('[itw] SSR npm scripts:')
|
|
159
|
+
if added:
|
|
160
|
+
for name, cmd in added:
|
|
161
|
+
print(f' + added: {name} → {cmd}')
|
|
162
|
+
if preserved:
|
|
163
|
+
for name in preserved:
|
|
164
|
+
print(f' = preserved: {name} (already in package.json, not overwritten)')
|
|
165
|
+
if skipped:
|
|
166
|
+
for name, cfg in skipped:
|
|
167
|
+
print(f" - skipped: {name} (no '{cfg}' config in angular.json)")
|
|
168
|
+
if not (added or preserved):
|
|
169
|
+
print(' (nothing added)')
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _pin_ssr_deps_in_package_json(target_version: str) -> list:
|
|
173
|
+
"""Pin @angular/ssr and @angular/platform-server in package.json to exact target_version.
|
|
174
|
+
|
|
175
|
+
Returns the list of packages whose entries were changed (or added). Does NOT touch
|
|
176
|
+
other @angular/* packages — only the two that @angular/ssr's install touches and
|
|
177
|
+
which carry strict peer-dependencies. This prevents npm from re-resolving them to
|
|
178
|
+
a newer patch that wouldn't match the installed @angular/core.
|
|
179
|
+
"""
|
|
180
|
+
pkg_path = os.path.join(os.getcwd(), 'package.json')
|
|
181
|
+
with open(pkg_path, 'r', encoding='utf-8') as fp:
|
|
182
|
+
data = json.load(fp)
|
|
183
|
+
deps = data.setdefault('dependencies', {})
|
|
184
|
+
changed = []
|
|
185
|
+
for pkg in ('@angular/ssr', '@angular/platform-server'):
|
|
186
|
+
if deps.get(pkg) != target_version:
|
|
187
|
+
deps[pkg] = target_version
|
|
188
|
+
changed.append(pkg)
|
|
189
|
+
if changed:
|
|
190
|
+
with open(pkg_path, 'w', encoding='utf-8') as fp:
|
|
191
|
+
json.dump(data, fp, indent=2)
|
|
192
|
+
fp.write('\n')
|
|
193
|
+
return changed
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
TEMPLATES_DIR = Path(__file__).parent / 'templates'
|
|
197
|
+
|
|
198
|
+
SITEMAP_IMPORT_LINE = "import {sitemapEntries} from './src/sitemap.routes';"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _load_template(name: str) -> str:
|
|
202
|
+
"""Read a template file, stripping the leading `// @ts-nocheck` editor hint."""
|
|
203
|
+
content = (TEMPLATES_DIR / name).read_text(encoding='utf-8')
|
|
204
|
+
lines = content.splitlines(keepends=True)
|
|
205
|
+
if lines and lines[0].lstrip().startswith('// @ts-nocheck'):
|
|
206
|
+
lines = lines[1:]
|
|
207
|
+
if lines and lines[0].strip() == '':
|
|
208
|
+
lines = lines[1:]
|
|
209
|
+
return ''.join(lines)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _run_ng_add_ssr(ctx: Context) -> None:
|
|
213
|
+
"""Pin SSR deps to the installed @angular/core version, then run `ng add @angular/ssr`.
|
|
214
|
+
|
|
215
|
+
Why: @angular/platform-server (pulled in by @angular/ssr) has strict per-patch
|
|
216
|
+
peer-deps — platform-server@X.Y.Z demands @angular/core@X.Y.Z *exactly*. If
|
|
217
|
+
package.json declares platform-server as a range like ^X.Y.0, npm resolves to
|
|
218
|
+
the latest patch (often newer than the installed core), which breaks the install.
|
|
219
|
+
|
|
220
|
+
The fix is to pin only the two new packages (@angular/ssr and @angular/platform-server)
|
|
221
|
+
in package.json to the exact patch that matches the installed @angular/core. This
|
|
222
|
+
leaves every other @angular/* package alone — no upgrades, no downgrades — and
|
|
223
|
+
lets ng add → npm install resolve cleanly without --legacy-peer-deps or --force.
|
|
224
|
+
|
|
225
|
+
Flags on `ng add`:
|
|
226
|
+
- Angular 18 and earlier: no `--server-routing` flag (schematic rejects it).
|
|
227
|
+
- Angular 19+: pass `--server-routing=false` to keep the classic setup.
|
|
228
|
+
"""
|
|
229
|
+
major = _angular_major_version()
|
|
230
|
+
installed_core = _angular_core_installed_version()
|
|
231
|
+
|
|
232
|
+
if installed_core:
|
|
233
|
+
changed = _pin_ssr_deps_in_package_json(installed_core)
|
|
234
|
+
if changed:
|
|
235
|
+
pkgs = ', '.join(changed)
|
|
236
|
+
print(f'[itw] Pinned {pkgs} to {installed_core} in package.json (matches installed @angular/core).')
|
|
237
|
+
package_spec = f'@angular/ssr@{installed_core}'
|
|
238
|
+
else:
|
|
239
|
+
package_spec = '@angular/ssr'
|
|
240
|
+
|
|
241
|
+
flags = '--skip-confirmation'
|
|
242
|
+
if major >= 19:
|
|
243
|
+
flags += ' --server-routing=false'
|
|
244
|
+
print(f'[itw] Running `ng add {package_spec}` (Angular {major}, this may take a minute)...')
|
|
245
|
+
ctx.run(f'npx ng add {package_spec} {flags}')
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _install_ngx_seo_helper(ctx: Context) -> None:
|
|
249
|
+
print('[itw] Installing ngx-seo-helper...')
|
|
250
|
+
ctx.run('npm install ngx-seo-helper')
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _write_sitemap_template() -> None:
|
|
254
|
+
path = os.path.join(os.getcwd(), 'src', 'sitemap.routes.ts')
|
|
255
|
+
if os.path.exists(path):
|
|
256
|
+
print(f'[itw] Skipping sitemap template — {path} already exists.')
|
|
257
|
+
return
|
|
258
|
+
with open(path, 'w', encoding='utf-8') as fp:
|
|
259
|
+
fp.write(_load_template('sitemap.routes.ts'))
|
|
260
|
+
print('[itw] Wrote src/sitemap.routes.ts (empty tree — fill in your routes).')
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _patch_server_ts() -> None:
|
|
264
|
+
"""Inject sitemap import + /sitemap.xml handler into server.ts (idempotent)."""
|
|
265
|
+
path = os.path.join(os.getcwd(), 'server.ts')
|
|
266
|
+
if not os.path.exists(path):
|
|
267
|
+
raise RuntimeError('server.ts missing after `ng add @angular/ssr` — cannot patch.')
|
|
268
|
+
|
|
269
|
+
with open(path, 'r', encoding='utf-8') as fp:
|
|
270
|
+
content = fp.read()
|
|
271
|
+
|
|
272
|
+
if 'sitemap.routes' in content:
|
|
273
|
+
print('[itw] server.ts already references sitemap.routes — skipping patch.')
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
lines = content.splitlines(keepends=True)
|
|
277
|
+
last_import_idx = -1
|
|
278
|
+
for i, line in enumerate(lines):
|
|
279
|
+
if line.startswith('import '):
|
|
280
|
+
last_import_idx = i
|
|
281
|
+
if last_import_idx == -1:
|
|
282
|
+
raise RuntimeError('server.ts has no imports — unexpected shape, aborting patch.')
|
|
283
|
+
lines.insert(last_import_idx + 1, SITEMAP_IMPORT_LINE + '\n')
|
|
284
|
+
content = ''.join(lines)
|
|
285
|
+
|
|
286
|
+
match = re.search(r"(server\.set\('views',\s*distFolder\);\s*\n)", content)
|
|
287
|
+
if not match:
|
|
288
|
+
match = re.search(r"(const\s+commonEngine\s*=\s*new\s+CommonEngine\(\);\s*\n)", content)
|
|
289
|
+
if not match:
|
|
290
|
+
raise RuntimeError(
|
|
291
|
+
'Could not find an injection point in server.ts (looked for `server.set(\'views\', ...)` '
|
|
292
|
+
'or `new CommonEngine()`). Patch sitemap handler manually.'
|
|
293
|
+
)
|
|
294
|
+
insert_at = match.end()
|
|
295
|
+
content = content[:insert_at] + _load_template('server.sitemap.snippet.ts') + content[insert_at:]
|
|
296
|
+
|
|
297
|
+
with open(path, 'w', encoding='utf-8') as fp:
|
|
298
|
+
fp.write(content)
|
|
299
|
+
print('[itw] Patched server.ts with /sitemap.xml handler.')
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _print_next_steps(project_name: str) -> None:
|
|
303
|
+
print()
|
|
304
|
+
print('=' * 70)
|
|
305
|
+
print('[itw] SSR scaffolding complete.')
|
|
306
|
+
print('=' * 70)
|
|
307
|
+
print('Next steps you must do manually:')
|
|
308
|
+
print()
|
|
309
|
+
print(' 1. Add `NgxSeoModule` to your AppModule imports.')
|
|
310
|
+
print(' 2. Fill in src/sitemap.routes.ts with your route tree.')
|
|
311
|
+
print(' 3. If `build:ssr:staging` was skipped above, add a `staging` configuration')
|
|
312
|
+
print(f' to angular.json (and the `server` target, if legacy builder), then re-run')
|
|
313
|
+
print(' `itw ssr-init --force` to add the missing scripts.')
|
|
314
|
+
print(' 4. Verify locally: npm run build:ssr && npm run serve:ssr')
|
|
315
|
+
print(' 5. Commit the new files.')
|
|
316
|
+
print(' 6. Deploy with: itw incrementrc --ssr (or incrementpatch/release --ssr)')
|
|
317
|
+
print('=' * 70)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@task(name='ssr-init')
|
|
321
|
+
def ssr_init(ctx: Context, force: bool = False) -> None:
|
|
322
|
+
"""One-time SSR scaffold: ng add @angular/ssr, ngx-seo-helper, sitemap stub, server.ts patch.
|
|
323
|
+
|
|
324
|
+
Auto-adds the build:ssr / build:ssr:staging / build:ssr:dev npm scripts based
|
|
325
|
+
on angular.json (detects builder type and available configs). Existing scripts
|
|
326
|
+
are never overwritten.
|
|
327
|
+
"""
|
|
328
|
+
if detect_project_type() != 'frontend':
|
|
329
|
+
raise RuntimeError('ssr-init only runs on frontend (Angular) projects.')
|
|
330
|
+
|
|
331
|
+
if is_ssr_project() and not force:
|
|
332
|
+
raise RuntimeError(
|
|
333
|
+
'SSR scaffolding already present (server.ts + tsconfig.server.json exist). '
|
|
334
|
+
'Re-run with --force to re-apply the sitemap stub and server.ts patch.'
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
name = _read_package_name()
|
|
338
|
+
print(f'[itw] Scaffolding SSR for project: {name}')
|
|
339
|
+
|
|
340
|
+
if not is_ssr_project():
|
|
341
|
+
_run_ng_add_ssr(ctx)
|
|
342
|
+
|
|
343
|
+
_maybe_add_ssr_scripts(ctx)
|
|
344
|
+
_install_ngx_seo_helper(ctx)
|
|
345
|
+
_write_sitemap_template()
|
|
346
|
+
_patch_server_ts()
|
|
347
|
+
|
|
348
|
+
_print_next_steps(name)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "itw_python_builder"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.32"
|
|
8
8
|
description = "Standardized Django deployment pipeline with Docker, testing, and SonarQube integration"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
"""SSR scaffolding tasks. One-time `itw ssr-init` for Angular SSR + ngx-seo-helper.
|
|
2
|
-
|
|
3
|
-
The build/version tasks (`incrementrc`, `release`, etc.) live in tasks.py and
|
|
4
|
-
take an `ssr=False` kwarg. With --ssr they invoke:
|
|
5
|
-
- `npm run build:ssr` on master
|
|
6
|
-
- `npm run build:ssr:staging` on any other branch
|
|
7
|
-
|
|
8
|
-
These two scripts are a contract the project owner defines in package.json.
|
|
9
|
-
ssr-init intentionally does NOT write them, because the right command depends
|
|
10
|
-
on which Angular SSR setup the project ended up with:
|
|
11
|
-
|
|
12
|
-
legacy browser builder → ng build --configuration=X && ng run <name>:server:X
|
|
13
|
-
modern application builder (Angular 17+, fresh `ng add @angular/ssr`)
|
|
14
|
-
→ ng build --configuration=X
|
|
15
|
-
"""
|
|
16
|
-
import json
|
|
17
|
-
import os
|
|
18
|
-
import re
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
|
-
from invoke import task, Context
|
|
22
|
-
|
|
23
|
-
from itw_python_builder.utils import detect_project_type, _read_package_name, is_ssr_project
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _angular_major_version() -> int:
|
|
27
|
-
"""Read @angular/core's major version from package.json. Returns 0 if not found."""
|
|
28
|
-
pkg_path = os.path.join(os.getcwd(), 'package.json')
|
|
29
|
-
with open(pkg_path, 'r', encoding='utf-8') as fp:
|
|
30
|
-
data = json.load(fp)
|
|
31
|
-
deps = {**data.get('dependencies', {}), **data.get('devDependencies', {})}
|
|
32
|
-
spec = deps.get('@angular/core', '')
|
|
33
|
-
match = re.match(r'[\^~>=<]*(\d+)', spec)
|
|
34
|
-
return int(match.group(1)) if match else 0
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _angular_core_installed_version() -> str:
|
|
38
|
-
"""Return the exact installed @angular/core version from node_modules, or '' if not found.
|
|
39
|
-
|
|
40
|
-
Used to pin `ng add @angular/ssr@<this>` so platform-server's strict peer-deps
|
|
41
|
-
(which demand the same patch across all @angular/* packages) align with what's
|
|
42
|
-
already installed — no need for --legacy-peer-deps or --force.
|
|
43
|
-
"""
|
|
44
|
-
pkg_path = os.path.join(os.getcwd(), 'node_modules', '@angular', 'core', 'package.json')
|
|
45
|
-
if not os.path.isfile(pkg_path):
|
|
46
|
-
return ''
|
|
47
|
-
with open(pkg_path, 'r', encoding='utf-8') as fp:
|
|
48
|
-
return json.load(fp).get('version', '')
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
TEMPLATES_DIR = Path(__file__).parent / 'templates'
|
|
52
|
-
|
|
53
|
-
SITEMAP_IMPORT_LINE = "import {sitemapEntries} from './src/sitemap.routes';"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _load_template(name: str) -> str:
|
|
57
|
-
"""Read a template file, stripping the leading `// @ts-nocheck` editor hint."""
|
|
58
|
-
content = (TEMPLATES_DIR / name).read_text(encoding='utf-8')
|
|
59
|
-
lines = content.splitlines(keepends=True)
|
|
60
|
-
if lines and lines[0].lstrip().startswith('// @ts-nocheck'):
|
|
61
|
-
lines = lines[1:]
|
|
62
|
-
if lines and lines[0].strip() == '':
|
|
63
|
-
lines = lines[1:]
|
|
64
|
-
return ''.join(lines)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def _run_ng_add_ssr(ctx: Context) -> None:
|
|
68
|
-
"""Run `ng add @angular/ssr` pinned to the project's installed @angular/core patch.
|
|
69
|
-
|
|
70
|
-
Angular's @angular/platform-server (pulled in by @angular/ssr) has strict
|
|
71
|
-
peer-dependencies — every patch version demands the exact same patch across
|
|
72
|
-
all @angular/* packages. Without pinning, `ng add` grabs the latest patch and
|
|
73
|
-
breaks any project not on that exact patch. Pinning to the installed core
|
|
74
|
-
version aligns everything cleanly without bypassing npm's resolver.
|
|
75
|
-
|
|
76
|
-
Flags:
|
|
77
|
-
- Angular 18 and earlier: no `--server-routing` flag (schematic rejects it).
|
|
78
|
-
- Angular 19+: pass `--server-routing=false` to keep the classic setup,
|
|
79
|
-
matching the layout itw expects (single `server` target, not the new
|
|
80
|
-
server router).
|
|
81
|
-
"""
|
|
82
|
-
major = _angular_major_version()
|
|
83
|
-
installed_core = _angular_core_installed_version()
|
|
84
|
-
if installed_core:
|
|
85
|
-
package_spec = f'@angular/ssr@{installed_core}'
|
|
86
|
-
else:
|
|
87
|
-
package_spec = '@angular/ssr'
|
|
88
|
-
flags = '--skip-confirmation'
|
|
89
|
-
if major >= 19:
|
|
90
|
-
flags += ' --server-routing=false'
|
|
91
|
-
print(f'[itw] Running `ng add {package_spec}` (Angular {major}, this may take a minute)...')
|
|
92
|
-
ctx.run(f'npx ng add {package_spec} {flags}')
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _install_ngx_seo_helper(ctx: Context) -> None:
|
|
96
|
-
print('[itw] Installing ngx-seo-helper...')
|
|
97
|
-
ctx.run('npm install ngx-seo-helper')
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _write_sitemap_template() -> None:
|
|
101
|
-
path = os.path.join(os.getcwd(), 'src', 'sitemap.routes.ts')
|
|
102
|
-
if os.path.exists(path):
|
|
103
|
-
print(f'[itw] Skipping sitemap template — {path} already exists.')
|
|
104
|
-
return
|
|
105
|
-
with open(path, 'w', encoding='utf-8') as fp:
|
|
106
|
-
fp.write(_load_template('sitemap.routes.ts'))
|
|
107
|
-
print('[itw] Wrote src/sitemap.routes.ts (empty tree — fill in your routes).')
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def _patch_server_ts() -> None:
|
|
111
|
-
"""Inject sitemap import + /sitemap.xml handler into server.ts (idempotent)."""
|
|
112
|
-
path = os.path.join(os.getcwd(), 'server.ts')
|
|
113
|
-
if not os.path.exists(path):
|
|
114
|
-
raise RuntimeError('server.ts missing after `ng add @angular/ssr` — cannot patch.')
|
|
115
|
-
|
|
116
|
-
with open(path, 'r', encoding='utf-8') as fp:
|
|
117
|
-
content = fp.read()
|
|
118
|
-
|
|
119
|
-
if 'sitemap.routes' in content:
|
|
120
|
-
print('[itw] server.ts already references sitemap.routes — skipping patch.')
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
lines = content.splitlines(keepends=True)
|
|
124
|
-
last_import_idx = -1
|
|
125
|
-
for i, line in enumerate(lines):
|
|
126
|
-
if line.startswith('import '):
|
|
127
|
-
last_import_idx = i
|
|
128
|
-
if last_import_idx == -1:
|
|
129
|
-
raise RuntimeError('server.ts has no imports — unexpected shape, aborting patch.')
|
|
130
|
-
lines.insert(last_import_idx + 1, SITEMAP_IMPORT_LINE + '\n')
|
|
131
|
-
content = ''.join(lines)
|
|
132
|
-
|
|
133
|
-
match = re.search(r"(server\.set\('views',\s*distFolder\);\s*\n)", content)
|
|
134
|
-
if not match:
|
|
135
|
-
match = re.search(r"(const\s+commonEngine\s*=\s*new\s+CommonEngine\(\);\s*\n)", content)
|
|
136
|
-
if not match:
|
|
137
|
-
raise RuntimeError(
|
|
138
|
-
'Could not find an injection point in server.ts (looked for `server.set(\'views\', ...)` '
|
|
139
|
-
'or `new CommonEngine()`). Patch sitemap handler manually.'
|
|
140
|
-
)
|
|
141
|
-
insert_at = match.end()
|
|
142
|
-
content = content[:insert_at] + _load_template('server.sitemap.snippet.ts') + content[insert_at:]
|
|
143
|
-
|
|
144
|
-
with open(path, 'w', encoding='utf-8') as fp:
|
|
145
|
-
fp.write(content)
|
|
146
|
-
print('[itw] Patched server.ts with /sitemap.xml handler.')
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def _print_next_steps(project_name: str) -> None:
|
|
150
|
-
print()
|
|
151
|
-
print('=' * 70)
|
|
152
|
-
print('[itw] SSR scaffolding complete.')
|
|
153
|
-
print('=' * 70)
|
|
154
|
-
print('Next steps you must do manually:')
|
|
155
|
-
print()
|
|
156
|
-
print(' 1. Add `NgxSeoModule` to your AppModule imports.')
|
|
157
|
-
print(' 2. Fill in src/sitemap.routes.ts with your route tree.')
|
|
158
|
-
print(' 3. Add `staging` (and optionally `development`) configurations to')
|
|
159
|
-
print(' angular.json under projects.' + project_name + '.architect.build.configurations.')
|
|
160
|
-
print(' ng add @angular/ssr only creates the `production` configuration.')
|
|
161
|
-
print()
|
|
162
|
-
print(' 4. Add these two npm scripts to package.json — the deploy pipeline')
|
|
163
|
-
print(' calls them by name:')
|
|
164
|
-
print()
|
|
165
|
-
print(' "build:ssr": "<build command for production>"')
|
|
166
|
-
print(' "build:ssr:staging": "<build command for staging>"')
|
|
167
|
-
print()
|
|
168
|
-
print(' The exact command depends on which builder angular.json uses:')
|
|
169
|
-
print()
|
|
170
|
-
print(' • Modern application builder (default for fresh ng add @angular/ssr):')
|
|
171
|
-
print(' "build:ssr": "ng build --configuration=production"')
|
|
172
|
-
print(' "build:ssr:staging": "ng build --configuration=staging"')
|
|
173
|
-
print()
|
|
174
|
-
print(' • Legacy browser builder (older projects with a separate server target):')
|
|
175
|
-
print(f' "build:ssr": "ng build --configuration=production && ng run {project_name}:server:production"')
|
|
176
|
-
print(f' "build:ssr:staging": "ng build --configuration=staging && ng run {project_name}:server:staging"')
|
|
177
|
-
print()
|
|
178
|
-
print(' 5. Verify locally: npm run build:ssr && npm run serve:ssr')
|
|
179
|
-
print(' 6. Commit the new files.')
|
|
180
|
-
print(' 7. Deploy with: itw incrementrc --ssr (or incrementpatch/release --ssr)')
|
|
181
|
-
print('=' * 70)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
@task(name='ssr-init')
|
|
185
|
-
def ssr_init(ctx: Context, force: bool = False) -> None:
|
|
186
|
-
"""One-time SSR scaffold: ng add @angular/ssr, ngx-seo-helper, sitemap stub, server.ts patch.
|
|
187
|
-
|
|
188
|
-
Does NOT write package.json scripts — those depend on which builder Angular
|
|
189
|
-
chose. The post-run message tells you the two scripts the pipeline expects.
|
|
190
|
-
"""
|
|
191
|
-
if detect_project_type() != 'frontend':
|
|
192
|
-
raise RuntimeError('ssr-init only runs on frontend (Angular) projects.')
|
|
193
|
-
|
|
194
|
-
if is_ssr_project() and not force:
|
|
195
|
-
raise RuntimeError(
|
|
196
|
-
'SSR scaffolding already present (server.ts + tsconfig.server.json exist). '
|
|
197
|
-
'Re-run with --force to re-apply the sitemap stub and server.ts patch.'
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
name = _read_package_name()
|
|
201
|
-
print(f'[itw] Scaffolding SSR for project: {name}')
|
|
202
|
-
|
|
203
|
-
if not is_ssr_project():
|
|
204
|
-
_run_ng_add_ssr(ctx)
|
|
205
|
-
|
|
206
|
-
_install_ngx_seo_helper(ctx)
|
|
207
|
-
_write_sitemap_template()
|
|
208
|
-
_patch_server_ts()
|
|
209
|
-
|
|
210
|
-
_print_next_steps(name)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder.egg-info/requires.txt
RENAMED
|
File without changes
|
{itw_python_builder-0.1.30 → itw_python_builder-0.1.32}/itw_python_builder.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|