create-substrat 0.12.13 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.js +27 -10
  2. package/package.json +5 -2
  3. package/windows.js +90 -0
package/index.js CHANGED
@@ -12,12 +12,13 @@
12
12
  * break at install time and damage the name it exists to protect.
13
13
  */
14
14
 
15
- import { cpSync, existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
15
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
16
16
  import { basename, dirname, join, resolve } from 'node:path';
17
17
  import { fileURLToPath } from 'node:url';
18
18
 
19
19
  import { DEV_SERVERS } from './dev-servers.js';
20
20
  import { TEST_RUNNER, TSCONFIG, VITEST_CONFIG } from './project-files.js';
21
+ import { offerLongPaths } from './windows.js';
21
22
 
22
23
  const HERE = dirname(fileURLToPath(import.meta.url));
23
24
  const TEMPLATE = join(HERE, 'template');
@@ -36,12 +37,12 @@ const TEMPLATE = join(HERE, 'template');
36
37
  // The runtime packages release together off one version line (the changesets `fixed`
37
38
  // group), so one constant is right for all of them. Engines do NOT share a line —
38
39
  // each versions on its own, so one pin per engine, deliberately.
39
- const SUBSTRAT = '^0.163.0';
40
- const ENGINE_WORKORDER = '^0.12.47';
41
- const ENGINE_INVOICING = '^0.11.47';
40
+ const SUBSTRAT = '^0.164.0';
41
+ const ENGINE_WORKORDER = '^0.12.48';
42
+ const ENGINE_INVOICING = '^0.11.48';
42
43
  const BOUNDARY_LINT = '^0.8.0';
43
- const DEV_ISSUER = '^0.2.45';
44
- const ATTACHMENT_EXTRACTORS = '^0.2.24';
44
+ const DEV_ISSUER = '^0.2.46';
45
+ const ATTACHMENT_EXTRACTORS = '^0.2.25';
45
46
 
46
47
  const DOCS = 'https://substrat.net';
47
48
 
@@ -70,7 +71,7 @@ function usage() {
70
71
  function toPackageName(dir) {
71
72
  const name = basename(resolve(dir))
72
73
  .toLowerCase()
73
- .replace(/[^a-z0-9-~]+/g, '-')
74
+ .replace(/[^a-z0-9-]+/g, '-')
74
75
  .replace(/^[-_.]+|[-_.]+$/g, '');
75
76
  return name || 'substrat-vertical';
76
77
  }
@@ -138,7 +139,13 @@ function packageJson(name) {
138
139
  // The local OIDC issuer `pnpm dev` signs you in at. A devDependency on
139
140
  // purpose: it is never imported by anything the worker bundles.
140
141
  '@substrat-run/dev-issuer': DEV_ISSUER,
141
- '@cloudflare/workers-types': '^4.20250109.0',
142
+ // What `substrat push` builds the Worker with. The CLI runs the project's own
143
+ // wrangler and never downloads one, so a push builds with the version pinned here.
144
+ wrangler: '^4.151.0',
145
+ // Major 5, because wrangler 4.151+ peers on it: with 4 here, `npm install` of a new
146
+ // project stops on ERESOLVE. The template compiles against 5 (checked by installing a
147
+ // scaffold from npm); the workspace catalog, which template-check reads, is still on 4.
148
+ '@cloudflare/workers-types': '^5.20261023.1',
142
149
  '@types/better-sqlite3': '^9.6.0',
143
150
  '@types/node': '^22.0.0',
144
151
  concurrently: '^9.0.0',
@@ -200,7 +207,7 @@ pnpm typecheck
200
207
  `;
201
208
  }
202
209
 
203
- function main() {
210
+ async function main() {
204
211
  const target = process.argv[2];
205
212
  if (target === '-h' || target === '--help') {
206
213
  usage();
@@ -227,6 +234,12 @@ function main() {
227
234
  // Generated configs — these need the project name, so they aren't in the template.
228
235
  const name = toPackageName(target);
229
236
  writeFileSync(join(dest, 'package.json'), packageJson(name));
237
+ // The gate key is the project's registry slug (the package name): a vertical may declare only
238
+ // its own slug and the engines' keys (#2388), so the template's `bikeshop` becomes the name.
239
+ for (const file of ['src/manifest.ts', 'src/provision.ts']) {
240
+ const path = join(dest, file);
241
+ writeFileSync(path, readFileSync(path, 'utf8').replace(/(entitlementKey: |ENTITLEMENT_KEYS = \[[^\]]*)'bikeshop'/, `$1'${name}'`));
242
+ }
230
243
  writeFileSync(join(dest, 'tsconfig.json'), TSCONFIG);
231
244
  writeFileSync(join(dest, 'vitest.config.ts'), VITEST_CONFIG);
232
245
  writeFileSync(join(dest, '.gitignore'), GITIGNORE);
@@ -250,6 +263,10 @@ function main() {
250
263
  '',
251
264
  ].join('\n'),
252
265
  );
266
+
267
+ // Windows only, and only when the setting is off: one question, then Windows' own prompt.
268
+ const longPaths = await offerLongPaths();
269
+ if (longPaths.length) process.stdout.write(`${longPaths.join('\n')}\n`);
253
270
  }
254
271
 
255
- main();
272
+ main().catch((error) => fail(error instanceof Error ? error.message : String(error)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-substrat",
3
- "version": "0.12.13",
3
+ "version": "0.13.0",
4
4
  "description": "Scaffold a Substrat vertical — `npm create substrat <dir>`.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -26,6 +26,7 @@
26
26
  "index.js",
27
27
  "dev-servers.js",
28
28
  "project-files.js",
29
+ "windows.js",
29
30
  "template"
30
31
  ],
31
32
  "engines": {
@@ -34,5 +35,7 @@
34
35
  "publishConfig": {
35
36
  "access": "public"
36
37
  },
37
- "scripts": {}
38
+ "scripts": {
39
+ "test": "node --test windows.test.mjs"
40
+ }
38
41
  }
package/windows.js ADDED
@@ -0,0 +1,90 @@
1
+ // Windows long paths, offered once when a project is scaffolded.
2
+ //
3
+ // A project's node_modules holds files deeper than Windows' old 260-character limit. Node, npm
4
+ // and pnpm go past it on their own; many other programs (PowerShell, Python, installers) stop
5
+ // there until the machine-wide `LongPathsEnabled` setting is on. Turning it on needs an
6
+ // administrator, so the scaffold asks first and then has Windows run the one registry write
7
+ // elevated: the person answers one question and Windows' own "allow this app" prompt.
8
+ // `substrat doctor --fix` (@substrat-run/cli, doctor.ts) does the same later; the two keep the
9
+ // same command and the same reading of `reg query`.
10
+
11
+ import { execFileSync } from 'node:child_process';
12
+ import { createInterface } from 'node:readline';
13
+
14
+ const FILESYSTEM_KEY = 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\FileSystem';
15
+
16
+ /** Every argument is a constant: nothing a person or a project supplies reaches the shell. */
17
+ export const ELEVATE_LONG_PATHS_SCRIPT =
18
+ 'Start-Process -FilePath reg.exe -Verb RunAs -Wait -WindowStyle Hidden -ArgumentList ' +
19
+ `'add','${FILESYSTEM_KEY}','/v','LongPathsEnabled','/t','REG_DWORD','/d','1','/f'`;
20
+
21
+ const LATER = 'npx @substrat-run/cli doctor --fix';
22
+
23
+ /** `reg query`'s answer: on, or off (an absent value is Windows' default, which is off). */
24
+ export function parseLongPaths(stdout) {
25
+ const m = stdout?.match(/LongPathsEnabled\s+REG_DWORD\s+0x([0-9a-f]+)/i);
26
+ return m ? parseInt(m[1], 16) !== 0 : false;
27
+ }
28
+
29
+ /** One question on the terminal; `undefined` when input ends (or Ctrl+C) before an answer. */
30
+ function ask(question) {
31
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
32
+ return new Promise((resolve) => {
33
+ rl.question(question, (answer) => {
34
+ resolve(answer.trim());
35
+ rl.close();
36
+ });
37
+ rl.on('close', () => resolve(undefined));
38
+ });
39
+ }
40
+
41
+ export const realSystem = () => ({
42
+ platform: process.platform,
43
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
44
+ query: () => {
45
+ try {
46
+ return execFileSync('reg.exe', ['query', FILESYSTEM_KEY, '/v', 'LongPathsEnabled'], {
47
+ encoding: 'utf8',
48
+ stdio: ['ignore', 'pipe', 'ignore'],
49
+ windowsHide: true,
50
+ });
51
+ } catch {
52
+ return undefined;
53
+ }
54
+ },
55
+ elevate: () => {
56
+ execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ELEVATE_LONG_PATHS_SCRIPT], {
57
+ stdio: ['ignore', 'ignore', 'pipe'],
58
+ windowsHide: true,
59
+ });
60
+ },
61
+ ask,
62
+ });
63
+
64
+ /**
65
+ * The lines to print after scaffolding, having offered to turn long paths on where that applies.
66
+ * Nothing on macOS and Linux, or when they are already on. Without a terminal to ask on (a coding
67
+ * agent) it changes nothing and says how to turn them on later. Never throws: a scaffold that
68
+ * has been written is not undone by a setting.
69
+ */
70
+ export async function offerLongPaths(sys = realSystem()) {
71
+ if (sys.platform !== 'win32') return [];
72
+ if (parseLongPaths(sys.query())) return [];
73
+ const why = [
74
+ ' Windows long paths are off. A project\'s node_modules holds files deeper than',
75
+ ' 260 characters, and many programs stop at that limit until it is turned on.',
76
+ ];
77
+ if (!sys.interactive) return [...why, ` Turn it on once (Windows asks you to allow it): ${LATER}`, ''];
78
+ const answer = await sys.ask([...why, ' Turn it on now? Windows will ask you to allow it. [Y/n] '].join('\n'));
79
+ if (answer === undefined || !(answer === '' || /^y/i.test(answer))) {
80
+ return ['', ` Left off. Turn it on later with: ${LATER}`, ''];
81
+ }
82
+ try {
83
+ sys.elevate();
84
+ } catch {
85
+ /* declined or failed: the re-read below says which */
86
+ }
87
+ return parseLongPaths(sys.query())
88
+ ? ['', ' ✓ Windows long paths turned on. Restart your terminal and editor so they see it.', '']
89
+ : ['', ` Long paths are still off (the permission prompt was declined). Later: ${LATER}`, ''];
90
+ }