@viglet/create-shio-app 2026.3.2 → 2026.3.3

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/LICENSE CHANGED
@@ -1,201 +1,201 @@
1
- Apache License
2
- Version 2.0, January 2004
3
- http://www.apache.org/licenses/
4
-
5
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
-
7
- 1. Definitions.
8
-
9
- "License" shall mean the terms and conditions for use, reproduction,
10
- and distribution as defined by Sections 1 through 9 of this document.
11
-
12
- "Licensor" shall mean the copyright owner or entity authorized by
13
- the copyright owner that is granting the License.
14
-
15
- "Legal Entity" shall mean the union of the acting entity and all
16
- other entities that control, are controlled by, or are under common
17
- control with that entity. For the purposes of this definition,
18
- "control" means (i) the power, direct or indirect, to cause the
19
- direction or management of such entity, whether by contract or
20
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
- outstanding shares, or (iii) beneficial ownership of such entity.
22
-
23
- "You" (or "Your") shall mean an individual or Legal Entity
24
- exercising permissions granted by this License.
25
-
26
- "Source" form shall mean the preferred form for making modifications,
27
- including but not limited to software source code, documentation
28
- source, and configuration files.
29
-
30
- "Object" form shall mean any form resulting from mechanical
31
- transformation or translation of a Source form, including but
32
- not limited to compiled object code, generated documentation,
33
- and conversions to other media types.
34
-
35
- "Work" shall mean the work of authorship, whether in Source or
36
- Object form, made available under the License, as indicated by a
37
- copyright notice that is included in or attached to the work
38
- (an example is provided in the Appendix below).
39
-
40
- "Derivative Works" shall mean any work, whether in Source or Object
41
- form, that is based on (or derived from) the Work and for which the
42
- editorial revisions, annotations, elaborations, or other modifications
43
- represent, as a whole, an original work of authorship. For the purposes
44
- of this License, Derivative Works shall not include works that remain
45
- separable from, or merely link (or bind by name) to the interfaces of,
46
- the Work and Derivative Works thereof.
47
-
48
- "Contribution" shall mean any work of authorship, including
49
- the original version of the Work and any modifications or additions
50
- to that Work or Derivative Works thereof, that is intentionally
51
- submitted to Licensor for inclusion in the Work by the copyright owner
52
- or by an individual or Legal Entity authorized to submit on behalf of
53
- the copyright owner. For the purposes of this definition, "submitted"
54
- means any form of electronic, verbal, or written communication sent
55
- to the Licensor or its representatives, including but not limited to
56
- communication on electronic mailing lists, source code control systems,
57
- and issue tracking systems that are managed by, or on behalf of, the
58
- Licensor for the purpose of discussing and improving the Work, but
59
- excluding communication that is conspicuously marked or otherwise
60
- designated in writing by the copyright owner as "Not a Contribution."
61
-
62
- "Contributor" shall mean Licensor and any individual or Legal Entity
63
- on behalf of whom a Contribution has been received by Licensor and
64
- subsequently incorporated within the Work.
65
-
66
- 2. Grant of Copyright License. Subject to the terms and conditions of
67
- this License, each Contributor hereby grants to You a perpetual,
68
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
- copyright license to reproduce, prepare Derivative Works of,
70
- publicly display, publicly perform, sublicense, and distribute the
71
- Work and such Derivative Works in Source or Object form.
72
-
73
- 3. Grant of Patent License. Subject to the terms and conditions of
74
- this License, each Contributor hereby grants to You a perpetual,
75
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
- (except as stated in this section) patent license to make, have made,
77
- use, offer to sell, sell, import, and otherwise transfer the Work,
78
- where such license applies only to those patent claims licensable
79
- by such Contributor that are necessarily infringed by their
80
- Contribution(s) alone or by combination of their Contribution(s)
81
- with the Work to which such Contribution(s) was submitted. If You
82
- institute patent litigation against any entity (including a
83
- cross-claim or counterclaim in a lawsuit) alleging that the Work
84
- or a Contribution incorporated within the Work constitutes direct
85
- or contributory patent infringement, then any patent licenses
86
- granted to You under this License for that Work shall terminate
87
- as of the date such litigation is filed.
88
-
89
- 4. Redistribution. You may reproduce and distribute copies of the
90
- Work or Derivative Works thereof in any medium, with or without
91
- modifications, and in Source or Object form, provided that You
92
- meet the following conditions:
93
-
94
- (a) You must give any other recipients of the Work or
95
- Derivative Works a copy of this License; and
96
-
97
- (b) You must cause any modified files to carry prominent notices
98
- stating that You changed the files; and
99
-
100
- (c) You must retain, in the Source form of any Derivative Works
101
- that You distribute, all copyright, patent, trademark, and
102
- attribution notices from the Source form of the Work,
103
- excluding those notices that do not pertain to any part of
104
- the Derivative Works; and
105
-
106
- (d) If the Work includes a "NOTICE" text file as part of its
107
- distribution, then any Derivative Works that You distribute must
108
- include a readable copy of the attribution notices contained
109
- within such NOTICE file, excluding those notices that do not
110
- pertain to any part of the Derivative Works, in at least one
111
- of the following places: within a NOTICE text file distributed
112
- as part of the Derivative Works; within the Source form or
113
- documentation, if provided along with the Derivative Works; or,
114
- within a display generated by the Derivative Works, if and
115
- wherever such third-party notices normally appear. The contents
116
- of the NOTICE file are for informational purposes only and
117
- do not modify the License. You may add Your own attribution
118
- notices within Derivative Works that You distribute, alongside
119
- or as an addendum to the NOTICE text from the Work, provided
120
- that such additional attribution notices cannot be construed
121
- as modifying the License.
122
-
123
- You may add Your own copyright statement to Your modifications and
124
- may provide additional or different license terms and conditions
125
- for use, reproduction, or distribution of Your modifications, or
126
- for any such Derivative Works as a whole, provided Your use,
127
- reproduction, and distribution of the Work otherwise complies with
128
- the conditions stated in this License.
129
-
130
- 5. Submission of Contributions. Unless You explicitly state otherwise,
131
- any Contribution intentionally submitted for inclusion in the Work
132
- by You to the Licensor shall be under the terms and conditions of
133
- this License, without any additional terms or conditions.
134
- Notwithstanding the above, nothing herein shall supersede or modify
135
- the terms of any separate license agreement you may have executed
136
- with Licensor regarding such Contributions.
137
-
138
- 6. Trademarks. This License does not grant permission to use the trade
139
- names, trademarks, service marks, or product names of the Licensor,
140
- except as required for reasonable and customary use in describing the
141
- origin of the Work and reproducing the content of the NOTICE file.
142
-
143
- 7. Disclaimer of Warranty. Unless required by applicable law or
144
- agreed to in writing, Licensor provides the Work (and each
145
- Contributor provides its Contributions) on an "AS IS" BASIS,
146
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
- implied, including, without limitation, any warranties or conditions
148
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
- PARTICULAR PURPOSE. You are solely responsible for determining the
150
- appropriateness of using or redistributing the Work and assume any
151
- risks associated with Your exercise of permissions under this License.
152
-
153
- 8. Limitation of Liability. In no event and under no legal theory,
154
- whether in tort (including negligence), contract, or otherwise,
155
- unless required by applicable law (such as deliberate and grossly
156
- negligent acts) or agreed to in writing, shall any Contributor be
157
- liable to You for damages, including any direct, indirect, special,
158
- incidental, or consequential damages of any character arising as a
159
- result of this License or out of the use or inability to use the
160
- Work (including but not limited to damages for loss of goodwill,
161
- work stoppage, computer failure or malfunction, or any and all
162
- other commercial damages or losses), even if such Contributor
163
- has been advised of the possibility of such damages.
164
-
165
- 9. Accepting Warranty or Additional Liability. While redistributing
166
- the Work or Derivative Works thereof, You may choose to offer,
167
- and charge a fee for, acceptance of support, warranty, indemnity,
168
- or other liability obligations and/or rights consistent with this
169
- License. However, in accepting such obligations, You may act only
170
- on Your own behalf and on Your sole responsibility, not on behalf
171
- of any other Contributor, and only if You agree to indemnify,
172
- defend, and hold each Contributor harmless for any liability
173
- incurred by, or claims asserted against, such Contributor by reason
174
- of your accepting any such warranty or additional liability.
175
-
176
- END OF TERMS AND CONDITIONS
177
-
178
- APPENDIX: How to apply the Apache License to your work.
179
-
180
- To apply the Apache License to your work, attach the following
181
- boilerplate notice, with the fields enclosed by brackets "[]"
182
- replaced with your own identifying information. (Don't include
183
- the brackets!) The text should be enclosed in the appropriate
184
- comment syntax for the file format. We also recommend that a
185
- file or class name and description of purpose be included on the
186
- same "printed page" as the copyright notice for easier
187
- identification within third-party archives.
188
-
189
- Copyright [yyyy] [name of copyright owner]
190
-
191
- Licensed under the Apache License, Version 2.0 (the "License");
192
- you may not use this file except in compliance with the License.
193
- You may obtain a copy of the License at
194
-
195
- http://www.apache.org/licenses/LICENSE-2.0
196
-
197
- Unless required by applicable law or agreed to in writing, software
198
- distributed under the License is distributed on an "AS IS" BASIS,
199
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
- See the License for the specific language governing permissions and
201
- limitations under the License.
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@viglet/create-shio-app?logo=npm)](https://www.npmjs.com/package/@viglet/create-shio-app)
4
4
  [![License](https://img.shields.io/npm/l/@viglet/create-shio-app)](https://github.com/openviglet/shio/blob/HEAD/LICENSE)
5
- [![Website](https://img.shields.io/badge/shio.viglet.org-0ea5e9?logo=googlechrome&logoColor=white)](https://shio.viglet.org/)
5
+ [![Website](https://img.shields.io/badge/website-0ea5e9?logo=googlechrome&logoColor=white)](https://openviglet.github.io/shio/)
6
6
 
7
7
  Scaffold a **Next.js (App Router)** site wired to the Content Delivery API of
8
- **[Viglet Shio](https://shio.viglet.org/)**, the open source **headless CMS built
8
+ **[Viglet Shio](https://openviglet.github.io/shio/)**, the open source, **agent-ready CMS
9
9
  for coding agents**.
10
10
 
11
11
  The flagship "JS dev, zero Java" starter — a JavaScript/TypeScript developer
@@ -92,7 +92,7 @@ npm test # node:test — scaffolds into a temp dir and asserts the output
92
92
 
93
93
  ## License
94
94
 
95
- Apache-2.0 — [Viglet Shio](https://shio.viglet.org/) ·
95
+ Apache-2.0 — [Viglet Shio](https://openviglet.github.io/shio/) ·
96
96
  [github.com/openviglet/shio](https://github.com/openviglet/shio) ·
97
97
  [docs.viglet.org/shio](https://docs.viglet.org/shio/) ·
98
98
  part of the [Viglet](https://www.viglet.org) open source family.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@viglet/create-shio-app",
3
- "version": "2026.3.2",
4
- "description": "Scaffold a Next.js (App Router) site wired to the Shio CMS Content Delivery API. The flagship 'JS dev, zero Java' starter.",
5
- "homepage": "https://shio.viglet.org/",
3
+ "version": "2026.3.3",
4
+ "description": "Scaffold a Next.js (App Router) site wired to the Viglet Shio Content Delivery API. The flagship 'JS dev, zero Java' starter.",
5
+ "homepage": "https://openviglet.github.io/shio/",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/openviglet/shio.git",
@@ -53,7 +53,7 @@
53
53
  "license": "Apache-2.0",
54
54
  "scripts": {
55
55
  "emit": "node src/emit.mjs",
56
- "test": "node --test \"test/*.test.mjs\"",
56
+ "test": "node --import ../scripts/test-watchdog.mjs --test \"test/*.test.mjs\"",
57
57
  "emit:check": "node scripts/emit-guard.mjs"
58
58
  }
59
59
  }
package/src/emit.mjs CHANGED
@@ -1,292 +1,292 @@
1
- /*
2
- * Copyright (C) 2016-2026 the original author or authors.
3
- *
4
- * Licensed under the Apache License, Version 2.0.
5
- *
6
- * SH119 — generate the `nextjs-starter` blueprint package from this package's
7
- * `template/` tree, so there is one starter with two ways to get it: offline
8
- * (`npm create shio-app`, which copies the tree) and from an instance
9
- * (`shio init --blueprint nextjs-starter`, which downloads the package's `files`).
10
- *
11
- * SH156 — and generate the section-vocabulary packages' front-end half the same way, as
12
- * **the same tree plus an overlay**. That is the answer to "how do packages share a tree
13
- * instead of each carrying a copy": they each carry a copy and *nobody writes one*. A
14
- * hand-maintained second starter would drift within a release; a generated one cannot,
15
- * because `test/emit.test.mjs` fails the build the moment it disagrees with its sources.
16
- *
17
- * Why generate rather than share. A running Shio reads blueprints off the classpath
18
- * and cannot run a JS build, so the bytes have to exist under
19
- * `shio-app/src/main/resources/blueprints/`. That is a second copy by necessity, which
20
- * is precisely why the drift test exists — the same discipline `shio-sections`' `emit`
21
- * uses for post-type defs.
22
- *
23
- * Run: `pnpm -C create-shio-app run emit`
24
- */
25
- import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
26
- import { existsSync } from "node:fs";
27
- import { dirname, join } from "node:path";
28
- import { fileURLToPath, pathToFileURL } from "node:url";
29
-
30
- import { rangeFor, unpublished } from "./published.mjs";
31
- import { readTemplateFiles } from "./scaffold.mjs";
32
-
33
- /** Where the blueprint packages live, relative to this file. */
34
- const BLUEPRINTS = fileURLToPath(
35
- new URL("../../shio-app/src/main/resources/blueprints/", import.meta.url)
36
- );
37
-
38
- /**
39
- * The section vocabulary's front-end half, authored once (SH156).
40
- *
41
- * <p>It lives in `shio-sections` rather than here because the vocabulary owns how it
42
- * renders — the same argument that puts its post-type defs in `src/post-types.ts`. This
43
- * package owns the *starter*, that one owns the *sections*, and `emit` is where they
44
- * meet.</p>
45
- */
46
- const SECTIONS_OVERLAY = fileURLToPath(new URL("../../shio-sections/front-end/", import.meta.url));
47
-
48
- /**
49
- * A package's own index page, one directory per package (SH216).
50
- *
51
- * <h2>Why these live here and the section overlay does not</h2>
52
- *
53
- * <p>The section overlay lives in `shio-sections` because <b>the vocabulary owns how it
54
- * renders</b> — the same argument that puts its post-type defs in `src/post-types.ts`. These have
55
- * no vocabulary. `blog`, `docs` and `product-catalog` are ordinary posts with an HTML field, and
56
- * what each is missing is one page the starter cannot provide because it is about the *model*
57
- * rather than about any page: the posts newest first, the documentation tree, the catalogue grid.
58
- * That belongs to whoever owns the starter, which is this package.</p>
59
- *
60
- * <p><b>Not beside the blueprint</b>, which was the tempting answer: `blueprints/<name>/files/` is
61
- * the tree `emit` <em>writes</em>, so a source overlay in the same directory would put input and
62
- * output in one place and make the drift test compare a thing with itself.</p>
63
- */
64
- const packageOverlay = (name) =>
65
- fileURLToPath(new URL(`../overlays/${name}/`, import.meta.url));
66
-
67
- /** `@viglet/shio-sections`' own package.json — the version the overlay depends on. */
68
- const SECTIONS_MANIFEST = fileURLToPath(
69
- new URL("../../shio-sections/package.json", import.meta.url)
70
- );
71
-
72
- /**
73
- * Every package generated from the starter tree.
74
- *
75
- * <p>Named explicitly rather than globbed, for the same reason `shio-sections`' target
76
- * list is: a package that stops carrying a front end is a decision, and it should look
77
- * like one here.</p>
78
- *
79
- * <p><b>`blog`, `docs` and `product-catalog` carry an index page instead of the section
80
- * overlay (SH216).</b> They do not compose sections — their models are ordinary posts with an
81
- * HTML field, which the starter's catch-all already renders — so the section tree would ship a
82
- * dependency they never import and let three packages claim a front end that renders their pages
83
- * no better than the plain starter does. What each was actually missing is one page the starter
84
- * cannot provide because it is about the <em>model</em>: a post list at `/blog`, a navigation tree
85
- * at `/docs`, a grid at `/products`. Three shapes, which is exactly why it is three overlays and
86
- * not one.</p>
87
- */
88
- export const PACKAGES = [
89
- { name: "nextjs-starter", overlay: null },
90
- { name: "marketing-site", overlay: SECTIONS_OVERLAY, needs: ["@viglet/shio-sections"] },
91
- { name: "landing", overlay: SECTIONS_OVERLAY, needs: ["@viglet/shio-sections"] },
92
- // No `needs`: an index page uses the SDK the starter already depends on. That is the whole
93
- // distinction from the section overlay, which brings a vocabulary with it.
94
- { name: "blog", overlay: packageOverlay("blog") },
95
- { name: "docs", overlay: packageOverlay("docs") },
96
- { name: "product-catalog", overlay: packageOverlay("product-catalog") },
97
- ];
98
-
99
- /** Where the carried tree lives inside a package. */
100
- const FILES_SUBDIR = "files";
101
-
102
- /** The starter's package directory — kept as a named export for SH119's callers. */
103
- export const PACKAGE_DIR = join(BLUEPRINTS, "nextjs-starter/");
104
-
105
- /**
106
- * The manifest's `files` refs for a carried tree.
107
- *
108
- * <p>`path` is where the file goes in the caller's project; `from` is where its bytes
109
- * live in the package. Both are always stated: the tree lives under `files/` so the
110
- * package root stays readable (manifest, model, content, tree), and the two names differ
111
- * for dotfiles anyway — a leading dot is stripped inside the package, which is the same
112
- * dodge the npm template uses for `gitignore` and which the `path` now undoes *in the
113
- * format* rather than in each consumer's code.</p>
114
- *
115
- * @param {Array<{path: string, text: string}>} files
116
- */
117
- export function fileRefs(files) {
118
- return files.map((file) => ({ path: file.path, from: sourcePath(file) }));
119
- }
120
-
121
- /** Where a row's bytes are written inside the package. */
122
- export function sourcePath(file) {
123
- return `${FILES_SUBDIR}/${file.path.replace(/^\./, "")}`;
124
- }
125
-
126
- /** Read an overlay directory as the same `{path, text}` rows the template produces. */
127
- export async function readOverlay(dir) {
128
- if (!dir || !existsSync(dir)) return [];
129
- const rows = [];
130
- for (const rel of await listFiles(dir)) {
131
- rows.push({ path: rel, text: await readFile(join(dir, rel), "utf8") });
132
- }
133
- return rows.sort((a, b) => a.path.localeCompare(b.path));
134
- }
135
-
136
- async function listFiles(dir, prefix = "") {
137
- const out = [];
138
- for (const entry of await readdir(dir, { withFileTypes: true })) {
139
- const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
140
- if (entry.isDirectory()) {
141
- out.push(...(await listFiles(join(dir, entry.name), rel)));
142
- } else {
143
- out.push(rel);
144
- }
145
- }
146
- return out;
147
- }
148
-
149
- /**
150
- * The tree one package carries: the starter, with the overlay's files replacing or
151
- * joining it, and the dependencies the overlay needs added to `package.json`.
152
- *
153
- * <p>Replace-by-path is what keeps the overlay small. The section-aware catch-all route
154
- * is the starter's route plus one branch, so it <em>is</em> `app/[...slug]/page.tsx`
155
- * rather than a second route the project would have to be told about.</p>
156
- *
157
- * @param {Array<{path: string, text: string}>} base the starter tree
158
- * @param {Array<{path: string, text: string}>} overlay files that replace or join it
159
- * @param {Record<string, string>} dependencies added to the project's package.json
160
- */
161
- export function compose(base, overlay, dependencies = {}) {
162
- const byPath = new Map(base.map((file) => [file.path, file]));
163
- for (const file of overlay) {
164
- byPath.set(file.path, file);
165
- }
166
- if (Object.keys(dependencies).length > 0) {
167
- const manifest = byPath.get("package.json");
168
- if (!manifest) {
169
- throw new Error("emit: the starter tree has no package.json to add dependencies to");
170
- }
171
- const parsed = JSON.parse(manifest.text);
172
- // Sorted, so adding one dependency does not reorder the ones already there and the
173
- // generated diff is the change and nothing else (P5).
174
- parsed.dependencies = Object.fromEntries(
175
- Object.entries({ ...parsed.dependencies, ...dependencies }).sort(([a], [b]) =>
176
- a.localeCompare(b)
177
- )
178
- );
179
- byPath.set("package.json", {
180
- path: "package.json",
181
- text: `${JSON.stringify(parsed, null, 2)}\n`,
182
- });
183
- }
184
- return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
185
- }
186
-
187
- /**
188
- * The dependency ranges an overlay's `needs` resolve to.
189
- *
190
- * <h3>The comment that used to be here was exactly backwards (SH446)</h3>
191
- *
192
- * <p>It read: "read from the workspace rather than written down here, so a release that bumps
193
- * `@viglet/shio-sections` cannot leave the generated packages pointing at a version nobody
194
- * published." Reading it from the workspace is what <em>guarantees</em> pointing at a version nobody
195
- * published — the workspace is the version being <em>built</em>, and it is installable only in the
196
- * instant after a release. Measured on `@viglet/shio-client`, whose template range was derived the
197
- * same way: `^2026.3.1` against a registry that had `2026.2.1`, so `npm install` in a fresh scaffold
198
- * failed `ETARGET` and nothing in the repository noticed, because nothing installs from the
199
- * registry.</p>
200
- *
201
- * <p>So the published range wins when there is one ({@link rangeFor}), and the workspace's is the
202
- * fallback for a package that has never been published at all — where no range is correct and
203
- * writing *something* at least leaves a project that is right the day the release runs.
204
- * {@link unpublishedNeeds} is what reports that state, and `emit` prints it.</p>
205
- */
206
- export async function resolveNeeds(needs) {
207
- if (!needs || needs.length === 0) return {};
208
- const { version } = JSON.parse(await readFile(SECTIONS_MANIFEST, "utf8"));
209
- return Object.fromEntries(needs.map((name) => [name, rangeFor(name) ?? `^${version}`]));
210
- }
211
-
212
- /** The `needs` across every package that name something the registry does not have (SH446). */
213
- export function unpublishedNeeds() {
214
- return unpublished([...new Set(PACKAGES.flatMap((pkg) => pkg.needs ?? []))]);
215
- }
216
-
217
- /** The whole tree one entry of {@link PACKAGES} should carry. */
218
- export async function filesFor(pkg, base) {
219
- const template = base ?? (await readTemplateFiles());
220
- if (!pkg.overlay) return template;
221
- return compose(template, await readOverlay(pkg.overlay), await resolveNeeds(pkg.needs));
222
- }
223
-
224
- /**
225
- * Read a package's manifest with its `files` list replaced by what its tree currently
226
- * holds — the shape `emit` would write. Pure, so the drift test can compare it against
227
- * what is on disk without touching the filesystem twice.
228
- */
229
- export async function expectedManifest(files, name = "nextjs-starter") {
230
- const manifest = JSON.parse(await readFile(join(BLUEPRINTS, name, "blueprint.json"), "utf8"));
231
- return { ...manifest, files: fileRefs(files) };
232
- }
233
-
234
- /** The bytes a package should carry, keyed by package-relative path. */
235
- export function expectedSources(files) {
236
- return new Map(files.map((file) => [sourcePath(file), file.text]));
237
- }
238
-
239
- export async function emit({ log = console.log } = {}) {
240
- const base = await readTemplateFiles();
241
- const emitted = {};
242
-
243
- for (const pkg of PACKAGES) {
244
- const dir = join(BLUEPRINTS, pkg.name);
245
- if (!existsSync(dir)) {
246
- throw new Error(
247
- `emit: the ${pkg.name} blueprint package is missing at ${dir}. ` +
248
- "The manifest is hand-authored (it declares the params and the promise); only " +
249
- "`files` and the carried tree are generated."
250
- );
251
- }
252
- const files = await filesFor(pkg, base);
253
-
254
- // Rewritten wholesale so a file deleted from the template disappears here too — a
255
- // stale leftover would be shipped to every project that applies the package.
256
- await rm(join(dir, FILES_SUBDIR), { recursive: true, force: true });
257
- for (const file of files) {
258
- const target = join(dir, sourcePath(file));
259
- await mkdir(dirname(target), { recursive: true });
260
- await writeFile(target, file.text, "utf8");
261
- }
262
-
263
- const manifest = await expectedManifest(files, pkg.name);
264
- await writeFile(join(dir, "blueprint.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
265
-
266
- log(`✓ emitted ${files.length} file(s) into blueprints/${pkg.name}`);
267
- emitted[pkg.name] = files.map((file) => file.path);
268
- }
269
-
270
- // SH446 — said at the moment the file is written, because the alternative is a stranger's
271
- // `npm install` saying it, and by then the scaffold is on their disk and this repository is not.
272
- const missing = unpublishedNeeds();
273
- if (missing.length) {
274
- log(
275
- `\n! ${missing.join(" ")} ${missing.length === 1 ? "is" : "are"} NOT on the registry, so the `
276
- + `starter${missing.length === 1 ? "" : "s"} carrying the section overlay cannot `
277
- + `npm install outside this monorepo.\n`
278
- + ` No range fixes it — publish them, then record the range in src/published.mjs.\n`
279
- + ` \`shio init\` (no --blueprint) is unaffected: it carries only published packages.\n`,
280
- );
281
- }
282
-
283
- return { files: emitted["nextjs-starter"], packages: emitted, unpublished: missing };
284
- }
285
-
286
- // `pathToFileURL`, not a hand-built `file://` string: on Windows `process.argv[1]` is
287
- // `D:\...` while `import.meta.url` is `file:///D:/...`, so the concatenated form never
288
- // matched and running the script did nothing at all — silently, which is the worst way for
289
- // a generator to fail.
290
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
291
- await emit();
292
- }
1
+ /*
2
+ * Copyright (C) 2016-2026 the original author or authors.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0.
5
+ *
6
+ * SH119 — generate the `nextjs-starter` blueprint package from this package's
7
+ * `template/` tree, so there is one starter with two ways to get it: offline
8
+ * (`npm create shio-app`, which copies the tree) and from an instance
9
+ * (`shio init --blueprint nextjs-starter`, which downloads the package's `files`).
10
+ *
11
+ * SH156 — and generate the section-vocabulary packages' front-end half the same way, as
12
+ * **the same tree plus an overlay**. That is the answer to "how do packages share a tree
13
+ * instead of each carrying a copy": they each carry a copy and *nobody writes one*. A
14
+ * hand-maintained second starter would drift within a release; a generated one cannot,
15
+ * because `test/emit.test.mjs` fails the build the moment it disagrees with its sources.
16
+ *
17
+ * Why generate rather than share. A running Shio reads blueprints off the classpath
18
+ * and cannot run a JS build, so the bytes have to exist under
19
+ * `shio-app/src/main/resources/blueprints/`. That is a second copy by necessity, which
20
+ * is precisely why the drift test exists — the same discipline `shio-sections`' `emit`
21
+ * uses for post-type defs.
22
+ *
23
+ * Run: `pnpm -C create-shio-app run emit`
24
+ */
25
+ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
26
+ import { existsSync } from "node:fs";
27
+ import { dirname, join } from "node:path";
28
+ import { fileURLToPath, pathToFileURL } from "node:url";
29
+
30
+ import { rangeFor, unpublished } from "./published.mjs";
31
+ import { readTemplateFiles } from "./scaffold.mjs";
32
+
33
+ /** Where the blueprint packages live, relative to this file. */
34
+ const BLUEPRINTS = fileURLToPath(
35
+ new URL("../../shio-app/src/main/resources/blueprints/", import.meta.url)
36
+ );
37
+
38
+ /**
39
+ * The section vocabulary's front-end half, authored once (SH156).
40
+ *
41
+ * <p>It lives in `shio-sections` rather than here because the vocabulary owns how it
42
+ * renders — the same argument that puts its post-type defs in `src/post-types.ts`. This
43
+ * package owns the *starter*, that one owns the *sections*, and `emit` is where they
44
+ * meet.</p>
45
+ */
46
+ const SECTIONS_OVERLAY = fileURLToPath(new URL("../../shio-sections/front-end/", import.meta.url));
47
+
48
+ /**
49
+ * A package's own index page, one directory per package (SH216).
50
+ *
51
+ * <h2>Why these live here and the section overlay does not</h2>
52
+ *
53
+ * <p>The section overlay lives in `shio-sections` because <b>the vocabulary owns how it
54
+ * renders</b> — the same argument that puts its post-type defs in `src/post-types.ts`. These have
55
+ * no vocabulary. `blog`, `docs` and `product-catalog` are ordinary posts with an HTML field, and
56
+ * what each is missing is one page the starter cannot provide because it is about the *model*
57
+ * rather than about any page: the posts newest first, the documentation tree, the catalogue grid.
58
+ * That belongs to whoever owns the starter, which is this package.</p>
59
+ *
60
+ * <p><b>Not beside the blueprint</b>, which was the tempting answer: `blueprints/<name>/files/` is
61
+ * the tree `emit` <em>writes</em>, so a source overlay in the same directory would put input and
62
+ * output in one place and make the drift test compare a thing with itself.</p>
63
+ */
64
+ const packageOverlay = (name) =>
65
+ fileURLToPath(new URL(`../overlays/${name}/`, import.meta.url));
66
+
67
+ /** `@viglet/shio-sections`' own package.json — the version the overlay depends on. */
68
+ const SECTIONS_MANIFEST = fileURLToPath(
69
+ new URL("../../shio-sections/package.json", import.meta.url)
70
+ );
71
+
72
+ /**
73
+ * Every package generated from the starter tree.
74
+ *
75
+ * <p>Named explicitly rather than globbed, for the same reason `shio-sections`' target
76
+ * list is: a package that stops carrying a front end is a decision, and it should look
77
+ * like one here.</p>
78
+ *
79
+ * <p><b>`blog`, `docs` and `product-catalog` carry an index page instead of the section
80
+ * overlay (SH216).</b> They do not compose sections — their models are ordinary posts with an
81
+ * HTML field, which the starter's catch-all already renders — so the section tree would ship a
82
+ * dependency they never import and let three packages claim a front end that renders their pages
83
+ * no better than the plain starter does. What each was actually missing is one page the starter
84
+ * cannot provide because it is about the <em>model</em>: a post list at `/blog`, a navigation tree
85
+ * at `/docs`, a grid at `/products`. Three shapes, which is exactly why it is three overlays and
86
+ * not one.</p>
87
+ */
88
+ export const PACKAGES = [
89
+ { name: "nextjs-starter", overlay: null },
90
+ { name: "marketing-site", overlay: SECTIONS_OVERLAY, needs: ["@viglet/shio-sections"] },
91
+ { name: "landing", overlay: SECTIONS_OVERLAY, needs: ["@viglet/shio-sections"] },
92
+ // No `needs`: an index page uses the SDK the starter already depends on. That is the whole
93
+ // distinction from the section overlay, which brings a vocabulary with it.
94
+ { name: "blog", overlay: packageOverlay("blog") },
95
+ { name: "docs", overlay: packageOverlay("docs") },
96
+ { name: "product-catalog", overlay: packageOverlay("product-catalog") },
97
+ ];
98
+
99
+ /** Where the carried tree lives inside a package. */
100
+ const FILES_SUBDIR = "files";
101
+
102
+ /** The starter's package directory — kept as a named export for SH119's callers. */
103
+ export const PACKAGE_DIR = join(BLUEPRINTS, "nextjs-starter/");
104
+
105
+ /**
106
+ * The manifest's `files` refs for a carried tree.
107
+ *
108
+ * <p>`path` is where the file goes in the caller's project; `from` is where its bytes
109
+ * live in the package. Both are always stated: the tree lives under `files/` so the
110
+ * package root stays readable (manifest, model, content, tree), and the two names differ
111
+ * for dotfiles anyway — a leading dot is stripped inside the package, which is the same
112
+ * dodge the npm template uses for `gitignore` and which the `path` now undoes *in the
113
+ * format* rather than in each consumer's code.</p>
114
+ *
115
+ * @param {Array<{path: string, text: string}>} files
116
+ */
117
+ export function fileRefs(files) {
118
+ return files.map((file) => ({ path: file.path, from: sourcePath(file) }));
119
+ }
120
+
121
+ /** Where a row's bytes are written inside the package. */
122
+ export function sourcePath(file) {
123
+ return `${FILES_SUBDIR}/${file.path.replace(/^\./, "")}`;
124
+ }
125
+
126
+ /** Read an overlay directory as the same `{path, text}` rows the template produces. */
127
+ export async function readOverlay(dir) {
128
+ if (!dir || !existsSync(dir)) return [];
129
+ const rows = [];
130
+ for (const rel of await listFiles(dir)) {
131
+ rows.push({ path: rel, text: await readFile(join(dir, rel), "utf8") });
132
+ }
133
+ return rows.sort((a, b) => a.path.localeCompare(b.path));
134
+ }
135
+
136
+ async function listFiles(dir, prefix = "") {
137
+ const out = [];
138
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
139
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
140
+ if (entry.isDirectory()) {
141
+ out.push(...(await listFiles(join(dir, entry.name), rel)));
142
+ } else {
143
+ out.push(rel);
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+
149
+ /**
150
+ * The tree one package carries: the starter, with the overlay's files replacing or
151
+ * joining it, and the dependencies the overlay needs added to `package.json`.
152
+ *
153
+ * <p>Replace-by-path is what keeps the overlay small. The section-aware catch-all route
154
+ * is the starter's route plus one branch, so it <em>is</em> `app/[...slug]/page.tsx`
155
+ * rather than a second route the project would have to be told about.</p>
156
+ *
157
+ * @param {Array<{path: string, text: string}>} base the starter tree
158
+ * @param {Array<{path: string, text: string}>} overlay files that replace or join it
159
+ * @param {Record<string, string>} dependencies added to the project's package.json
160
+ */
161
+ export function compose(base, overlay, dependencies = {}) {
162
+ const byPath = new Map(base.map((file) => [file.path, file]));
163
+ for (const file of overlay) {
164
+ byPath.set(file.path, file);
165
+ }
166
+ if (Object.keys(dependencies).length > 0) {
167
+ const manifest = byPath.get("package.json");
168
+ if (!manifest) {
169
+ throw new Error("emit: the starter tree has no package.json to add dependencies to");
170
+ }
171
+ const parsed = JSON.parse(manifest.text);
172
+ // Sorted, so adding one dependency does not reorder the ones already there and the
173
+ // generated diff is the change and nothing else (P5).
174
+ parsed.dependencies = Object.fromEntries(
175
+ Object.entries({ ...parsed.dependencies, ...dependencies }).sort(([a], [b]) =>
176
+ a.localeCompare(b)
177
+ )
178
+ );
179
+ byPath.set("package.json", {
180
+ path: "package.json",
181
+ text: `${JSON.stringify(parsed, null, 2)}\n`,
182
+ });
183
+ }
184
+ return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
185
+ }
186
+
187
+ /**
188
+ * The dependency ranges an overlay's `needs` resolve to.
189
+ *
190
+ * <h3>The comment that used to be here was exactly backwards (SH446)</h3>
191
+ *
192
+ * <p>It read: "read from the workspace rather than written down here, so a release that bumps
193
+ * `@viglet/shio-sections` cannot leave the generated packages pointing at a version nobody
194
+ * published." Reading it from the workspace is what <em>guarantees</em> pointing at a version nobody
195
+ * published — the workspace is the version being <em>built</em>, and it is installable only in the
196
+ * instant after a release. Measured on `@viglet/shio-client`, whose template range was derived the
197
+ * same way: `^2026.3.1` against a registry that had `2026.2.1`, so `npm install` in a fresh scaffold
198
+ * failed `ETARGET` and nothing in the repository noticed, because nothing installs from the
199
+ * registry.</p>
200
+ *
201
+ * <p>So the published range wins when there is one ({@link rangeFor}), and the workspace's is the
202
+ * fallback for a package that has never been published at all — where no range is correct and
203
+ * writing *something* at least leaves a project that is right the day the release runs.
204
+ * {@link unpublishedNeeds} is what reports that state, and `emit` prints it.</p>
205
+ */
206
+ export async function resolveNeeds(needs) {
207
+ if (!needs || needs.length === 0) return {};
208
+ const { version } = JSON.parse(await readFile(SECTIONS_MANIFEST, "utf8"));
209
+ return Object.fromEntries(needs.map((name) => [name, rangeFor(name) ?? `^${version}`]));
210
+ }
211
+
212
+ /** The `needs` across every package that name something the registry does not have (SH446). */
213
+ export function unpublishedNeeds() {
214
+ return unpublished([...new Set(PACKAGES.flatMap((pkg) => pkg.needs ?? []))]);
215
+ }
216
+
217
+ /** The whole tree one entry of {@link PACKAGES} should carry. */
218
+ export async function filesFor(pkg, base) {
219
+ const template = base ?? (await readTemplateFiles());
220
+ if (!pkg.overlay) return template;
221
+ return compose(template, await readOverlay(pkg.overlay), await resolveNeeds(pkg.needs));
222
+ }
223
+
224
+ /**
225
+ * Read a package's manifest with its `files` list replaced by what its tree currently
226
+ * holds — the shape `emit` would write. Pure, so the drift test can compare it against
227
+ * what is on disk without touching the filesystem twice.
228
+ */
229
+ export async function expectedManifest(files, name = "nextjs-starter") {
230
+ const manifest = JSON.parse(await readFile(join(BLUEPRINTS, name, "blueprint.json"), "utf8"));
231
+ return { ...manifest, files: fileRefs(files) };
232
+ }
233
+
234
+ /** The bytes a package should carry, keyed by package-relative path. */
235
+ export function expectedSources(files) {
236
+ return new Map(files.map((file) => [sourcePath(file), file.text]));
237
+ }
238
+
239
+ export async function emit({ log = console.log } = {}) {
240
+ const base = await readTemplateFiles();
241
+ const emitted = {};
242
+
243
+ for (const pkg of PACKAGES) {
244
+ const dir = join(BLUEPRINTS, pkg.name);
245
+ if (!existsSync(dir)) {
246
+ throw new Error(
247
+ `emit: the ${pkg.name} blueprint package is missing at ${dir}. ` +
248
+ "The manifest is hand-authored (it declares the params and the promise); only " +
249
+ "`files` and the carried tree are generated."
250
+ );
251
+ }
252
+ const files = await filesFor(pkg, base);
253
+
254
+ // Rewritten wholesale so a file deleted from the template disappears here too — a
255
+ // stale leftover would be shipped to every project that applies the package.
256
+ await rm(join(dir, FILES_SUBDIR), { recursive: true, force: true });
257
+ for (const file of files) {
258
+ const target = join(dir, sourcePath(file));
259
+ await mkdir(dirname(target), { recursive: true });
260
+ await writeFile(target, file.text, "utf8");
261
+ }
262
+
263
+ const manifest = await expectedManifest(files, pkg.name);
264
+ await writeFile(join(dir, "blueprint.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
265
+
266
+ log(`✓ emitted ${files.length} file(s) into blueprints/${pkg.name}`);
267
+ emitted[pkg.name] = files.map((file) => file.path);
268
+ }
269
+
270
+ // SH446 — said at the moment the file is written, because the alternative is a stranger's
271
+ // `npm install` saying it, and by then the scaffold is on their disk and this repository is not.
272
+ const missing = unpublishedNeeds();
273
+ if (missing.length) {
274
+ log(
275
+ `\n! ${missing.join(" ")} ${missing.length === 1 ? "is" : "are"} NOT on the registry, so the `
276
+ + `starter${missing.length === 1 ? "" : "s"} carrying the section overlay cannot `
277
+ + `npm install outside this monorepo.\n`
278
+ + ` No range fixes it — publish them, then record the range in src/published.mjs.\n`
279
+ + ` \`shio init\` (no --blueprint) is unaffected: it carries only published packages.\n`,
280
+ );
281
+ }
282
+
283
+ return { files: emitted["nextjs-starter"], packages: emitted, unpublished: missing };
284
+ }
285
+
286
+ // `pathToFileURL`, not a hand-built `file://` string: on Windows `process.argv[1]` is
287
+ // `D:\...` while `import.meta.url` is `file:///D:/...`, so the concatenated form never
288
+ // matched and running the script did nothing at all — silently, which is the worst way for
289
+ // a generator to fail.
290
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
291
+ await emit();
292
+ }
package/src/published.mjs CHANGED
@@ -40,15 +40,24 @@
40
40
  * A `null` is not a placeholder to fill in with a guess: it means the package has no version on the
41
41
  * registry at all, so **no range is correct** and any scaffold carrying it cannot install. It is
42
42
  * recorded rather than hidden so the failure has a name, and `unpublished()` is what reports it.
43
+ *
44
+ * The `null` form is kept now that nothing uses it (SH502). It is the shape the next unpublished
45
+ * package needs, and the two readers that branch on it — `init`'s warning and the SH448 conformance
46
+ * — are written against `unpublished()` rather than against a fixed list, so they already work in
47
+ * both worlds. Deleting it would mean rediscovering the answer the next time a package is added
48
+ * ahead of its release, which is the situation these two rows spent two blocks in.
43
49
  */
44
50
  export const PUBLISHED = {
45
51
  "@viglet/shio-client": "^2026.2.1",
46
- // SH156 made these publishable and no release has run, so both answer E404. The section-carrying
47
- // starters (`marketing-site`, `landing`) import the vocabulary, so they cannot be made installable
48
- // by choosing a different range — only by publishing. Left declared so the gap is a row rather
49
- // than an absence.
50
- "@viglet/shio-model": null,
51
- "@viglet/shio-sections": null,
52
+ // SH502 — these were `null` for two blocks: SH156 made them publishable and no release had run, so
53
+ // both answered E404 and the section-carrying starters (`marketing-site`, `landing`), which import
54
+ // the vocabulary, could not be made installable by choosing a different range. Only by publishing.
55
+ //
56
+ // The release landed on 2026-08-12 at `2026.3.2` and both now resolve. Caret on the published
57
+ // version, the same rule `@viglet/shio-client` follows one line up and for the same reason: it
58
+ // resolves today and picks up the next release without this file being edited again.
59
+ "@viglet/shio-model": "^2026.3.2",
60
+ "@viglet/shio-sections": "^2026.3.2",
52
61
  };
53
62
 
54
63
  /** The packages a scaffold would need and the registry does not have. */
@@ -1,6 +1,6 @@
1
1
  # {{projectName}}
2
2
 
3
- A [Next.js](https://nextjs.org) (App Router) site powered by **Shio CMS** via the
3
+ A [Next.js](https://nextjs.org) (App Router) site powered by **Viglet Shio** via the
4
4
  Content Delivery API — headless, TypeScript-first, **zero Java**.
5
5
 
6
6
  ## Getting started
@@ -26,6 +26,7 @@ Content Delivery API — headless, TypeScript-first, **zero Java**.
26
26
  - [`lib/shio.ts`](lib/shio.ts) creates a shared [`@viglet/shio-client`](https://www.npmjs.com/package/@viglet/shio-client) instance from the environment.
27
27
  - [`app/page.tsx`](app/page.tsx) lists the site's published posts (`shio.query`).
28
28
  - [`app/[...slug]/page.tsx`](app/[...slug]/page.tsx) resolves any path to a post via `shio.getPostByUrl(siteId, "/the/path")` — friendly-URL routing straight from the CDA, with `generateStaticParams` (SSG) + `revalidate` (ISR).
29
+ - [`middleware.ts`](middleware.ts) answers a path that moved. Redirects written in Shio (a `Redirect` post, or keeping an old address working when a page is renamed) come from the CDA's `/site/{siteId}/redirects` table, held for 60 seconds, and a moved path gets its `301` or `302` before any page is looked up. The logic is in [`lib/redirects.ts`](lib/redirects.ts).
29
30
 
30
31
  All content comes from the Shio CDA (`/api/v2/cda/**`). You never touch the Java backend — just edit React/TypeScript.
31
32
 
@@ -104,5 +105,5 @@ by hand.
104
105
 
105
106
  ## Learn more
106
107
 
107
- - [Shio CMS](https://github.com/openviglet/shio)
108
+ - [Viglet Shio](https://github.com/openviglet/shio)
108
109
  - [Next.js Docs](https://nextjs.org/docs)
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
2
2
 
3
3
  export const metadata = {
4
4
  title: "Shio site",
5
- description: "Powered by Shio CMS — headless, JavaScript-first.",
5
+ description: "Powered by Viglet Shio — headless, JavaScript-first.",
6
6
  };
7
7
 
8
8
  export default function RootLayout({ children }: { children: ReactNode }) {
@@ -24,3 +24,9 @@ yarn-error.log*
24
24
  # typescript
25
25
  *.tsbuildinfo
26
26
  next-env.d.ts
27
+
28
+ # shio: the CLI's derived state — a clone's progress and inventory, a conversion
29
+ # proposal, and SH1117's conditional-read cache. None of it is content, all of it
30
+ # is reproducible, and the read cache holds response bodies that belong to nobody's
31
+ # history.
32
+ /shio/.shio/
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The site's redirect table, consulted before a page is looked up (Shio roadmap SH1133).
3
+ *
4
+ * A redirect written in Shio — a `Redirect` post, or the "keep the old address working" offer when
5
+ * a page is renamed — is honoured by Shio's own `/sites/**` delivery. This front end serves the
6
+ * site's URLs itself, so it has to do the redirecting: `middleware.ts` asks here first.
7
+ *
8
+ * Plain TypeScript with no `next` import, so it can be tested with `node --test` directly.
9
+ */
10
+
11
+ /** One row of `GET /api/v2/cda/site/{siteId}/redirects`. */
12
+ export interface Redirect {
13
+ from: string;
14
+ to: string;
15
+ permanent: boolean;
16
+ }
17
+
18
+ /** Where a moved path goes, and with which status. */
19
+ export interface Moved {
20
+ destination: string;
21
+ status: 301 | 302;
22
+ }
23
+
24
+ /**
25
+ * The spelling Shio matches paths in: repeated slashes collapsed, one leading slash, no trailing
26
+ * one (the root stays `/`). Case is kept, as it is on the server.
27
+ */
28
+ export function canonicalPath(path: string): string {
29
+ const collapsed = ("/" + (path ?? "")).replace(/\/{2,}/g, "/");
30
+ // Repeated slashes are already collapsed, so there is at most one to take off the end.
31
+ return collapsed.length > 1 ? collapsed.replace(/\/$/, "") : collapsed;
32
+ }
33
+
34
+ /** The row for this path, or `null` when it did not move. */
35
+ export function findRedirect(
36
+ table: readonly Redirect[] | null | undefined,
37
+ path: string
38
+ ): Moved | null {
39
+ const wanted = canonicalPath(path);
40
+ for (const row of table ?? []) {
41
+ if (row.to && canonicalPath(row.from) === wanted) {
42
+ return { destination: row.to, status: row.permanent ? 301 : 302 };
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ /**
49
+ * A cached lookup over a table loaded by `load`.
50
+ *
51
+ * The table is small and changes rarely, so it is held for `ttlMs` (the same 60 seconds the pages'
52
+ * ISR window uses) rather than fetched per request. A load that fails keeps the last table it had —
53
+ * an unreachable CMS must never turn every request into an error — and is retried after the same
54
+ * interval rather than on the next request.
55
+ */
56
+ export function redirectLookup(
57
+ load: () => Promise<readonly Redirect[] | null>,
58
+ ttlMs = 60_000,
59
+ now: () => number = Date.now
60
+ ): (path: string) => Promise<Moved | null> {
61
+ let table: readonly Redirect[] = [];
62
+ let loadedAt = Number.NEGATIVE_INFINITY;
63
+ // Requests arriving while a load is in flight wait for that load rather than starting their own.
64
+ let pending: Promise<void> | null = null;
65
+ return async (path: string) => {
66
+ if (!pending && now() - loadedAt >= ttlMs) {
67
+ loadedAt = now();
68
+ pending = load()
69
+ .then((loaded) => {
70
+ table = loaded ?? [];
71
+ })
72
+ .catch(() => {
73
+ // Keep the last table: see the note above.
74
+ })
75
+ .finally(() => {
76
+ pending = null;
77
+ });
78
+ }
79
+ if (pending) await pending;
80
+ return findRedirect(table, path);
81
+ };
82
+ }
83
+
84
+ /**
85
+ * The loader `middleware.ts` uses: the CDA's redirect table for one site.
86
+ *
87
+ * It calls the endpoint with `fetch` rather than through `@viglet/shio-client`, because the SDK's
88
+ * `getRedirects` is newer than the release this template installs. A Shio instance that predates the
89
+ * endpoint answers 404, which is an empty table here rather than an error.
90
+ */
91
+ export function cdaRedirects(
92
+ baseUrl: string,
93
+ siteId: string,
94
+ token?: string
95
+ ): () => Promise<Redirect[] | null> {
96
+ return async () => {
97
+ if (!siteId) return null;
98
+ const url = `${baseUrl.replace(/\/+$/, "")}/api/v2/cda/site/${encodeURIComponent(siteId)}/redirects`;
99
+ const response = await fetch(url, {
100
+ headers: { Accept: "application/json", ...(token ? { Key: token } : {}) },
101
+ });
102
+ if (response.status === 404) return [];
103
+ if (!response.ok) throw new Error(`redirect table: HTTP ${response.status}`);
104
+ return (await response.json()) as Redirect[];
105
+ };
106
+ }
@@ -0,0 +1,31 @@
1
+ import { NextResponse, type NextRequest } from "next/server";
2
+ import { cdaRedirects, redirectLookup } from "./lib/redirects";
3
+
4
+ /**
5
+ * Answers a path that moved with the redirect written for it in Shio (roadmap SH1133).
6
+ *
7
+ * Shio's own delivery honours its redirects; this site does not go through that delivery, so the
8
+ * table is consulted here, before any page is looked up. A redirect's source is its own address in
9
+ * Shio, so it never shadows a page: the CMS refuses a redirect over a page that exists.
10
+ */
11
+ const moved = redirectLookup(
12
+ cdaRedirects(
13
+ process.env.SHIO_URL ?? "http://localhost:2710",
14
+ process.env.SHIO_SITE_ID ?? "",
15
+ process.env.SHIO_CDA_TOKEN
16
+ )
17
+ );
18
+
19
+ export async function middleware(request: NextRequest) {
20
+ const found = await moved(request.nextUrl.pathname);
21
+ if (!found) return NextResponse.next();
22
+ // A relative destination is on this site; an absolute one is passed through as written.
23
+ const destination = new URL(found.destination, request.url);
24
+ if (!destination.search) destination.search = request.nextUrl.search;
25
+ return NextResponse.redirect(destination, found.status);
26
+ }
27
+
28
+ export const config = {
29
+ // Everything but Next's own assets and this app's route handlers; a moved path may end in `.html`.
30
+ matcher: ["/((?!_next/|api/).*)"],
31
+ };