create-warlock 5.17.1 → 5.19.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.
- package/CHANGELOG.md +15 -0
- package/README.md +9 -7
- package/esm/commands/create-warlock-app/index.mjs +9 -1
- package/esm/commands/create-warlock-app/index.mjs.map +1 -1
- package/esm/helpers/app.mjs +11 -0
- package/esm/helpers/app.mjs.map +1 -1
- package/esm/helpers/pnpm-build-approvals.mjs +46 -0
- package/esm/helpers/pnpm-build-approvals.mjs.map +1 -0
- package/llms-full.txt +7 -1
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/create-a-warlock-project/SKILL.md +7 -1
- package/templates/warlock/AGENTS.md +1 -1
- package/templates/warlock/skills/module-boundaries/SKILL.md +8 -0
- package/templates/warlock/skills/web-code-standards/SKILL.md +38 -0
- package/templates/warlock/src/web/404.page.tsx +0 -7
- package/templates/warlock/src/web/404.setup.ts +8 -0
- package/templates/warlock/src/web/home/index.page.tsx +3 -30
- package/templates/warlock/src/web/home/{register.ts → index.setup.ts} +26 -0
- package/templates/warlock/src/web/root.setup.ts +4 -0
- package/templates/warlock/src/web/root.tsx +1 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to `create-warlock` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
+
## 5.19.0 - 2026-09-23
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Refined package skill-discovery descriptions and regenerated the llms projections.
|
|
12
|
+
- The default Web starter now keeps root, page, and not-found server exports in
|
|
13
|
+
paired `*.setup.ts` modules. Components remain presentation-only and import
|
|
14
|
+
the home loader type-only, while the setup module retains the universal
|
|
15
|
+
localization `register()` projection.
|
|
16
|
+
- PNPM image scaffolds approve only Sharp's required native build in the
|
|
17
|
+
generated `pnpm-workspace.yaml` before installing the selected feature. An
|
|
18
|
+
existing explicit denial is preserved. If selected feature dependencies fail
|
|
19
|
+
to install, JWT generation is reported as skipped instead of retrying through
|
|
20
|
+
the package manager.
|
|
21
|
+
|
|
7
22
|
## 5.17.0 - 2026-09-21
|
|
8
23
|
|
|
9
24
|
### Changed
|
package/README.md
CHANGED
|
@@ -106,13 +106,15 @@ not from source.
|
|
|
106
106
|
|
|
107
107
|
`.github/workflows/ci.yml` runs two jobs:
|
|
108
108
|
|
|
109
|
-
- **`specs`** — on pushes to `main`/`master` and on pull requests: `node scripts/check-resolver-boundaries.mjs`
|
|
110
|
-
(below), a smoke test that the built CLI's `--version` output matches `package.json`, then
|
|
111
|
-
`
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
`
|
|
109
|
+
- **`specs`** — on pushes to `main`/`master` and on pull requests: `node scripts/check-resolver-boundaries.mjs`
|
|
110
|
+
(below), a smoke test that the built CLI's `--version` output matches `package.json`, then
|
|
111
|
+
`npm run test:standalone`. This excludes only the source-bound template typecheck, which
|
|
112
|
+
deliberately requires sibling framework repositories and remains available through the default
|
|
113
|
+
`npm test` / `npm run typecheck:template` workspace lane.
|
|
114
|
+
- **`scaffold-typecheck`** — scaffolds a real project with the current scaffolder, installs
|
|
115
|
+
it, and runs that project's own `tsc --noEmit` (`npm run typecheck:scaffold`; see the
|
|
116
|
+
header of `scripts/typecheck-scaffold.mjs` for why this can't be done by typechecking
|
|
117
|
+
`templates/warlock/` in place). This registry-backed consumer check remains mandatory.
|
|
116
118
|
|
|
117
119
|
Both jobs also run nightly (`cron "0 4 * * *"`) and on `workflow_dispatch`, in addition to
|
|
118
120
|
push/PR — `scaffold-typecheck` installs the framework from the registry, so a framework
|
|
@@ -65,6 +65,7 @@ async function createWarlockApp(application) {
|
|
|
65
65
|
...aiProviders
|
|
66
66
|
];
|
|
67
67
|
const failedFeatures = [];
|
|
68
|
+
let featureDependencyInstallFailed = false;
|
|
68
69
|
if (selectedFeatures.length > 0) {
|
|
69
70
|
const featuresSpinner = spinner();
|
|
70
71
|
featuresSpinner.start(spinnerMessages.addingFeatures);
|
|
@@ -81,6 +82,7 @@ async function createWarlockApp(application) {
|
|
|
81
82
|
let featureInstall;
|
|
82
83
|
if (addedFeatures.length > 0) {
|
|
83
84
|
application.pinViteResolution();
|
|
85
|
+
application.approveSelectedFeatureBuilds(addedFeatures);
|
|
84
86
|
const install = application.install();
|
|
85
87
|
const installed = await install.install;
|
|
86
88
|
featureInstall = await install.result;
|
|
@@ -91,6 +93,7 @@ async function createWarlockApp(application) {
|
|
|
91
93
|
reason
|
|
92
94
|
});
|
|
93
95
|
addedFeatures = [];
|
|
96
|
+
featureDependencyInstallFailed = true;
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
if (failedFeatures.length === 0) featuresSpinner.stop(spinnerMessages.featuresAdded);
|
|
@@ -119,7 +122,12 @@ async function createWarlockApp(application) {
|
|
|
119
122
|
});
|
|
120
123
|
}
|
|
121
124
|
}
|
|
122
|
-
if (useJWT) {
|
|
125
|
+
if (useJWT && featureDependencyInstallFailed) problems.push({
|
|
126
|
+
step: "JWT secrets",
|
|
127
|
+
detail: "Skipped because the selected feature dependencies did not install.",
|
|
128
|
+
hints: [`Install the failed feature dependencies, then run \`${runPackageManagerCommand("jwt")}\` inside the project.`]
|
|
129
|
+
});
|
|
130
|
+
else if (useJWT) {
|
|
123
131
|
const jwtSpinner = spinner();
|
|
124
132
|
jwtSpinner.start(spinnerMessages.generatingJwt);
|
|
125
133
|
const command = runPackageManagerCommand("jwt");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../create-warlock/src/commands/create-warlock-app/index.ts"],"sourcesContent":["import {\n getDatabaseLabel,\n isNoDatabase,\n} from \"../../features/database-drivers\";\nimport { App } from \"../../helpers/app\";\nimport { CommandResult, takeLastCommandOutput } from \"../../helpers/exec\";\nimport {\n getPackageManager,\n runPackageManagerCommand,\n} from \"../../helpers/package-manager\";\nimport { resolveWarlockVersions } from \"../../helpers/warlock-versions\";\nimport { printDecisionsSummary } from \"../../summary\";\nimport { showSuccessScreen } from \"../../ui/banner\";\nimport {\n failFatally,\n installFailureHints,\n Problem,\n showNotes,\n showPartialScreen,\n showProblems,\n} from \"../../ui/report\";\nimport { spinner } from \"../../ui/spinner\";\nimport { spinnerMessages } from \"../../ui/spinners\";\n\n/**\n * What the scaffold actually achieved. `ok` is false when ANY step the user\n * asked for did not happen; the caller turns that into a non-zero exit code so\n * a script never mistakes a half-built project for a finished one.\n */\nexport type ScaffoldOutcome = {\n ok: boolean;\n problems: Problem[];\n};\n\n/** One-line \"why did this fail\" pulled out of a captured command. */\nfunction reasonFrom(result: CommandResult | undefined): string {\n if (!result) return \"the command reported a failure (no output captured)\";\n\n if (result.error) {\n return result.error.message || String(result.error);\n }\n\n const lastLine = `${result.stderr}\\n${result.stdout}`\n .split(/\\r?\\n/)\n .map(line => line.trim())\n .filter(Boolean)\n .pop();\n\n const status =\n result.code === null\n ? `killed by ${result.signal ?? \"an unknown signal\"}`\n : `exit code ${result.code}`;\n\n return lastLine ? `${status} — ${lastLine}` : status;\n}\n\nexport async function createWarlockApp(\n application: App,\n): Promise<ScaffoldOutcome> {\n const options = application.options;\n const { useGit, useJWT, features, aiProviders, databaseDriver } = options;\n const noDatabase = isNoDatabase(databaseDriver);\n const problems: Problem[] = [];\n\n // Resolve the versions to pin BEFORE anything is written. The scaffolder's\n // own version is only used when the registry confirms it exists — see\n // helpers/warlock-versions.ts for why an unverified pin broke every install.\n const { versions, notes } = await resolveWarlockVersions();\n\n // Step 1: Initialize and copy template\n const templateSpinner = spinner();\n templateSpinner.start(spinnerMessages.copyingTemplate);\n\n try {\n application\n .init()\n .use(\"warlock\")\n .updatePackageJson(versions)\n .updateDotEnv();\n\n // Wire the chosen database driver into .env — or, when the user opted out,\n // strip the database config entirely so the app boots with no database.\n if (noDatabase) {\n application.removeDatabaseConfig();\n } else {\n application.configureDatabaseEnv(databaseDriver);\n application.configureDatabaseConfig(databaseDriver);\n }\n\n application.configureWebStarter(features.includes(\"web\"));\n application.configureAgentKitTargets(options.agents ?? [\"claude\"]);\n } catch (error) {\n templateSpinner.stop(spinnerMessages.templateFailed);\n\n failFatally({\n step: \"Template copy\",\n detail: `The project files could not be written: ${(error as Error).message}`,\n hints: [\n \"Check that the target directory is writable and that no file is locked by another process.\",\n ],\n });\n }\n\n templateSpinner.stop(spinnerMessages.templateCopied);\n\n showNotes(notes);\n\n // Step 2: Install base dependencies (so the `warlock` binary is available).\n // Nothing downstream works without this, so a failure ends the run — loudly,\n // with the command, its exit code and its output.\n const installSpinner = spinner();\n installSpinner.start(spinnerMessages.installingDeps);\n\n const baseInstall = application.install();\n const baseInstalled = await baseInstall.install;\n const baseInstallResult = await baseInstall.result;\n\n if (!baseInstalled) {\n installSpinner.stop(spinnerMessages.depsFailed);\n\n failFatally({\n step: \"Dependency install\",\n detail: \"The project's dependencies were not installed.\",\n result: baseInstallResult,\n hints: installFailureHints(baseInstallResult),\n });\n }\n\n installSpinner.stop(spinnerMessages.depsInstalled);\n\n // Step 3: Add features via `warlock add --no-install`, then one batched install.\n // The DB driver, optional features, and AI providers all go through the single\n // source of truth (core's feature map) so versions never drift. When no\n // database was chosen, the driver is omitted (there is no `none` feature).\n const selectedFeatures = [\n ...(noDatabase ? [] : [databaseDriver]),\n ...features,\n ...aiProviders,\n ];\n\n // Features the user asked for that are not in the project when we finish.\n const failedFeatures: { feature: string; reason: string }[] = [];\n\n if (selectedFeatures.length > 0) {\n const featuresSpinner = spinner();\n featuresSpinner.start(spinnerMessages.addingFeatures);\n\n let addedFeatures = selectedFeatures;\n\n if (!(await application.installFeatures(selectedFeatures))) {\n // The batch is all-or-nothing, so it cannot say WHICH feature broke it.\n // Retry them one at a time: every feature that can be added still gets\n // added, and every one that cannot gets its own reason.\n const batchFailure = takeLastCommandOutput();\n\n addedFeatures = [];\n\n for (const feature of selectedFeatures) {\n if (await application.installFeatures([feature])) {\n addedFeatures.push(feature);\n } else {\n failedFeatures.push({\n feature,\n reason: reasonFrom(takeLastCommandOutput() ?? batchFailure),\n });\n }\n }\n }\n\n let featureInstall: CommandResult | undefined;\n\n if (addedFeatures.length > 0) {\n // `warlock add` records the feature dependencies but does not reconcile\n // them with each other. The `web` feature brings `vite` while the\n // template always brings `vitest`, whose own vite range resolves to a\n // different major — two copies, and yarn 1 aborts the whole link phase\n // on them. Pin one copy BEFORE the batched install, not after.\n application.pinViteResolution();\n\n const install = application.install();\n const installed = await install.install;\n\n featureInstall = await install.result;\n\n if (!installed) {\n // The dependencies were recorded but never fetched — the features are\n // not usable, so they are failures, not successes.\n const reason = `their packages were not installed (${reasonFrom(featureInstall)})`;\n\n for (const feature of addedFeatures) {\n failedFeatures.push({ feature, reason });\n }\n\n addedFeatures = [];\n }\n }\n\n if (failedFeatures.length === 0) {\n featuresSpinner.stop(spinnerMessages.featuresAdded);\n } else {\n featuresSpinner.stop(\n addedFeatures.length > 0\n ? spinnerMessages.featuresPartial\n : spinnerMessages.featuresFailed,\n );\n\n problems.push({\n step: \"Features\",\n detail: failedFeatures\n .map(({ feature, reason }) => `${feature}: ${reason}`)\n .join(\"\\n \"),\n result: featureInstall?.ok === false ? featureInstall : undefined,\n hints: [\n `Retry inside the project with: npx warlock add ${failedFeatures\n .map(({ feature }) => feature)\n .join(\" \")}`,\n ],\n });\n }\n }\n\n // Step 4: Initialize Git repository if requested\n if (useGit) {\n const gitSpinner = spinner();\n gitSpinner.start(spinnerMessages.initializingGit);\n\n const initialized = await application.git();\n\n gitSpinner.stop(\n initialized ? spinnerMessages.gitInitialized : spinnerMessages.gitFailed,\n );\n\n if (!initialized) {\n const failure = takeLastCommandOutput();\n\n problems.push({\n step: \"Git repository\",\n detail: `The repository was not initialized: ${reasonFrom(failure)}`,\n result: failure,\n hints: [\n \"Check that git is installed and that user.name / user.email are configured, then run `git init` yourself.\",\n ],\n });\n }\n }\n\n // Step 5: Generate JWT or warm cache\n if (useJWT) {\n const jwtSpinner = spinner();\n jwtSpinner.start(spinnerMessages.generatingJwt);\n\n const command = runPackageManagerCommand(\"jwt\");\n const generated = await application.exec(command);\n\n jwtSpinner.stop(\n generated ? spinnerMessages.jwtGenerated : spinnerMessages.jwtFailed,\n );\n\n if (!generated) {\n const failure = takeLastCommandOutput();\n\n problems.push({\n step: \"JWT secrets\",\n detail: `No JWT secrets were written to .env: ${reasonFrom(failure)}`,\n result: failure,\n hints: [`Run \\`${command}\\` inside the project before starting it.`],\n });\n }\n } else {\n // The warm cache is a start-up optimisation, not something the user asked\n // for: report it, but it does not make the scaffold a failure.\n const warmSpinner = spinner();\n warmSpinner.start(spinnerMessages.warmingCache);\n\n const warmed = await application.exec(\"npx warlock --warm-cache\");\n\n warmSpinner.stop(\n warmed ? spinnerMessages.cacheWarmed : spinnerMessages.cacheWarmFailed,\n );\n\n if (!warmed) takeLastCommandOutput();\n }\n\n // Step 6: Report what actually happened — the summary may only advertise\n // features that are really in the project.\n const requestedFeatures = [...features, ...aiProviders];\n const missingFeatures = failedFeatures\n .map(({ feature }) => feature)\n .filter(feature => requestedFeatures.includes(feature));\n const installedFeatures = requestedFeatures.filter(\n feature => !missingFeatures.includes(feature),\n );\n\n if (problems.length > 0) {\n showProblems(problems);\n\n showPartialScreen({\n projectName: application.name,\n database: getDatabaseLabel(databaseDriver),\n features: installedFeatures,\n missingFeatures,\n packageManager: getPackageManager(),\n });\n\n printDecisionsSummary({ packageManager: getPackageManager(), options });\n\n return { ok: false, problems };\n }\n\n showSuccessScreen({\n projectName: application.name,\n database: getDatabaseLabel(databaseDriver),\n features: installedFeatures,\n packageManager: getPackageManager(),\n });\n\n printDecisionsSummary({ packageManager: getPackageManager(), options });\n\n return { ok: true, problems };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../create-warlock/src/commands/create-warlock-app/index.ts"],"sourcesContent":["import {\n getDatabaseLabel,\n isNoDatabase,\n} from \"../../features/database-drivers\";\nimport { App } from \"../../helpers/app\";\nimport { CommandResult, takeLastCommandOutput } from \"../../helpers/exec\";\nimport {\n getPackageManager,\n runPackageManagerCommand,\n} from \"../../helpers/package-manager\";\nimport { resolveWarlockVersions } from \"../../helpers/warlock-versions\";\nimport { printDecisionsSummary } from \"../../summary\";\nimport { showSuccessScreen } from \"../../ui/banner\";\nimport {\n failFatally,\n installFailureHints,\n Problem,\n showNotes,\n showPartialScreen,\n showProblems,\n} from \"../../ui/report\";\nimport { spinner } from \"../../ui/spinner\";\nimport { spinnerMessages } from \"../../ui/spinners\";\n\n/**\n * What the scaffold actually achieved. `ok` is false when ANY step the user\n * asked for did not happen; the caller turns that into a non-zero exit code so\n * a script never mistakes a half-built project for a finished one.\n */\nexport type ScaffoldOutcome = {\n ok: boolean;\n problems: Problem[];\n};\n\n/** One-line \"why did this fail\" pulled out of a captured command. */\nfunction reasonFrom(result: CommandResult | undefined): string {\n if (!result) return \"the command reported a failure (no output captured)\";\n\n if (result.error) {\n return result.error.message || String(result.error);\n }\n\n const lastLine = `${result.stderr}\\n${result.stdout}`\n .split(/\\r?\\n/)\n .map(line => line.trim())\n .filter(Boolean)\n .pop();\n\n const status =\n result.code === null\n ? `killed by ${result.signal ?? \"an unknown signal\"}`\n : `exit code ${result.code}`;\n\n return lastLine ? `${status} — ${lastLine}` : status;\n}\n\nexport async function createWarlockApp(\n application: App,\n): Promise<ScaffoldOutcome> {\n const options = application.options;\n const { useGit, useJWT, features, aiProviders, databaseDriver } = options;\n const noDatabase = isNoDatabase(databaseDriver);\n const problems: Problem[] = [];\n\n // Resolve the versions to pin BEFORE anything is written. The scaffolder's\n // own version is only used when the registry confirms it exists — see\n // helpers/warlock-versions.ts for why an unverified pin broke every install.\n const { versions, notes } = await resolveWarlockVersions();\n\n // Step 1: Initialize and copy template\n const templateSpinner = spinner();\n templateSpinner.start(spinnerMessages.copyingTemplate);\n\n try {\n application\n .init()\n .use(\"warlock\")\n .updatePackageJson(versions)\n .updateDotEnv();\n\n // Wire the chosen database driver into .env — or, when the user opted out,\n // strip the database config entirely so the app boots with no database.\n if (noDatabase) {\n application.removeDatabaseConfig();\n } else {\n application.configureDatabaseEnv(databaseDriver);\n application.configureDatabaseConfig(databaseDriver);\n }\n\n application.configureWebStarter(features.includes(\"web\"));\n application.configureAgentKitTargets(options.agents ?? [\"claude\"]);\n } catch (error) {\n templateSpinner.stop(spinnerMessages.templateFailed);\n\n failFatally({\n step: \"Template copy\",\n detail: `The project files could not be written: ${(error as Error).message}`,\n hints: [\n \"Check that the target directory is writable and that no file is locked by another process.\",\n ],\n });\n }\n\n templateSpinner.stop(spinnerMessages.templateCopied);\n\n showNotes(notes);\n\n // Step 2: Install base dependencies (so the `warlock` binary is available).\n // Nothing downstream works without this, so a failure ends the run — loudly,\n // with the command, its exit code and its output.\n const installSpinner = spinner();\n installSpinner.start(spinnerMessages.installingDeps);\n\n const baseInstall = application.install();\n const baseInstalled = await baseInstall.install;\n const baseInstallResult = await baseInstall.result;\n\n if (!baseInstalled) {\n installSpinner.stop(spinnerMessages.depsFailed);\n\n failFatally({\n step: \"Dependency install\",\n detail: \"The project's dependencies were not installed.\",\n result: baseInstallResult,\n hints: installFailureHints(baseInstallResult),\n });\n }\n\n installSpinner.stop(spinnerMessages.depsInstalled);\n\n // Step 3: Add features via `warlock add --no-install`, then one batched install.\n // The DB driver, optional features, and AI providers all go through the single\n // source of truth (core's feature map) so versions never drift. When no\n // database was chosen, the driver is omitted (there is no `none` feature).\n const selectedFeatures = [\n ...(noDatabase ? [] : [databaseDriver]),\n ...features,\n ...aiProviders,\n ];\n\n // Features the user asked for that are not in the project when we finish.\n const failedFeatures: { feature: string; reason: string }[] = [];\n let featureDependencyInstallFailed = false;\n\n if (selectedFeatures.length > 0) {\n const featuresSpinner = spinner();\n featuresSpinner.start(spinnerMessages.addingFeatures);\n\n let addedFeatures = selectedFeatures;\n\n if (!(await application.installFeatures(selectedFeatures))) {\n // The batch is all-or-nothing, so it cannot say WHICH feature broke it.\n // Retry them one at a time: every feature that can be added still gets\n // added, and every one that cannot gets its own reason.\n const batchFailure = takeLastCommandOutput();\n\n addedFeatures = [];\n\n for (const feature of selectedFeatures) {\n if (await application.installFeatures([feature])) {\n addedFeatures.push(feature);\n } else {\n failedFeatures.push({\n feature,\n reason: reasonFrom(takeLastCommandOutput() ?? batchFailure),\n });\n }\n }\n }\n\n let featureInstall: CommandResult | undefined;\n\n if (addedFeatures.length > 0) {\n // `warlock add` records the feature dependencies but does not reconcile\n // them with each other. The `web` feature brings `vite` while the\n // template always brings `vitest`, whose own vite range resolves to a\n // different major — two copies, and yarn 1 aborts the whole link phase\n // on them. Pin one copy BEFORE the batched install, not after.\n application.pinViteResolution();\n application.approveSelectedFeatureBuilds(addedFeatures);\n\n const install = application.install();\n const installed = await install.install;\n\n featureInstall = await install.result;\n\n if (!installed) {\n // The dependencies were recorded but never fetched — the features are\n // not usable, so they are failures, not successes.\n const reason = `their packages were not installed (${reasonFrom(featureInstall)})`;\n\n for (const feature of addedFeatures) {\n failedFeatures.push({ feature, reason });\n }\n\n addedFeatures = [];\n featureDependencyInstallFailed = true;\n }\n }\n\n if (failedFeatures.length === 0) {\n featuresSpinner.stop(spinnerMessages.featuresAdded);\n } else {\n featuresSpinner.stop(\n addedFeatures.length > 0\n ? spinnerMessages.featuresPartial\n : spinnerMessages.featuresFailed,\n );\n\n problems.push({\n step: \"Features\",\n detail: failedFeatures\n .map(({ feature, reason }) => `${feature}: ${reason}`)\n .join(\"\\n \"),\n result: featureInstall?.ok === false ? featureInstall : undefined,\n hints: [\n `Retry inside the project with: npx warlock add ${failedFeatures\n .map(({ feature }) => feature)\n .join(\" \")}`,\n ],\n });\n }\n }\n\n // Step 4: Initialize Git repository if requested\n if (useGit) {\n const gitSpinner = spinner();\n gitSpinner.start(spinnerMessages.initializingGit);\n\n const initialized = await application.git();\n\n gitSpinner.stop(\n initialized ? spinnerMessages.gitInitialized : spinnerMessages.gitFailed,\n );\n\n if (!initialized) {\n const failure = takeLastCommandOutput();\n\n problems.push({\n step: \"Git repository\",\n detail: `The repository was not initialized: ${reasonFrom(failure)}`,\n result: failure,\n hints: [\n \"Check that git is installed and that user.name / user.email are configured, then run `git init` yourself.\",\n ],\n });\n }\n }\n\n // Step 5: Generate JWT or warm cache\n if (useJWT && featureDependencyInstallFailed) {\n problems.push({\n step: \"JWT secrets\",\n detail:\n \"Skipped because the selected feature dependencies did not install.\",\n hints: [\n `Install the failed feature dependencies, then run \\`${runPackageManagerCommand(\"jwt\")}\\` inside the project.`,\n ],\n });\n } else if (useJWT) {\n const jwtSpinner = spinner();\n jwtSpinner.start(spinnerMessages.generatingJwt);\n\n const command = runPackageManagerCommand(\"jwt\");\n const generated = await application.exec(command);\n\n jwtSpinner.stop(\n generated ? spinnerMessages.jwtGenerated : spinnerMessages.jwtFailed,\n );\n\n if (!generated) {\n const failure = takeLastCommandOutput();\n\n problems.push({\n step: \"JWT secrets\",\n detail: `No JWT secrets were written to .env: ${reasonFrom(failure)}`,\n result: failure,\n hints: [`Run \\`${command}\\` inside the project before starting it.`],\n });\n }\n } else {\n // The warm cache is a start-up optimisation, not something the user asked\n // for: report it, but it does not make the scaffold a failure.\n const warmSpinner = spinner();\n warmSpinner.start(spinnerMessages.warmingCache);\n\n const warmed = await application.exec(\"npx warlock --warm-cache\");\n\n warmSpinner.stop(\n warmed ? spinnerMessages.cacheWarmed : spinnerMessages.cacheWarmFailed,\n );\n\n if (!warmed) takeLastCommandOutput();\n }\n\n // Step 6: Report what actually happened — the summary may only advertise\n // features that are really in the project.\n const requestedFeatures = [...features, ...aiProviders];\n const missingFeatures = failedFeatures\n .map(({ feature }) => feature)\n .filter(feature => requestedFeatures.includes(feature));\n const installedFeatures = requestedFeatures.filter(\n feature => !missingFeatures.includes(feature),\n );\n\n if (problems.length > 0) {\n showProblems(problems);\n\n showPartialScreen({\n projectName: application.name,\n database: getDatabaseLabel(databaseDriver),\n features: installedFeatures,\n missingFeatures,\n packageManager: getPackageManager(),\n });\n\n printDecisionsSummary({ packageManager: getPackageManager(), options });\n\n return { ok: false, problems };\n }\n\n showSuccessScreen({\n projectName: application.name,\n database: getDatabaseLabel(databaseDriver),\n features: installedFeatures,\n packageManager: getPackageManager(),\n });\n\n printDecisionsSummary({ packageManager: getPackageManager(), options });\n\n return { ok: true, problems };\n}\n"],"mappings":"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"}
|
package/esm/helpers/app.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { getPackageManager } from "./package-manager.mjs";
|
|
|
3
3
|
import { executeCommand, runCommand } from "./exec.mjs";
|
|
4
4
|
import { insertImportInSortedPosition } from "./insert-import.mjs";
|
|
5
5
|
import { packageRoot, template } from "./paths.mjs";
|
|
6
|
+
import { mergePnpmBuildApprovals, requiredPnpmBuildApprovals } from "./pnpm-build-approvals.mjs";
|
|
6
7
|
import { fallbackRange } from "./warlock-versions.mjs";
|
|
7
8
|
import { copyDirectory, copyFile, fileExists, getFile, getJsonFile, putFile, putJsonFile, renameFile } from "@warlock.js/fs";
|
|
8
9
|
import { existsSync, rmSync, unlinkSync } from "node:fs";
|
|
@@ -45,6 +46,16 @@ var App = class {
|
|
|
45
46
|
install() {
|
|
46
47
|
return runCommand(getPackageManager(), ["install"], this.path, { env: installEnvironment() });
|
|
47
48
|
}
|
|
49
|
+
/** Record native build approvals before pnpm installs selected feature dependencies. */
|
|
50
|
+
approveSelectedFeatureBuilds(features) {
|
|
51
|
+
if (getPackageManager() !== "pnpm") return this;
|
|
52
|
+
const packages = requiredPnpmBuildApprovals(features);
|
|
53
|
+
if (packages.length === 0) return this;
|
|
54
|
+
const workspacePath = path.join(this.path, "pnpm-workspace.yaml");
|
|
55
|
+
if (!fileExists(workspacePath)) return this;
|
|
56
|
+
putFile(workspacePath, mergePnpmBuildApprovals(getFile(workspacePath), packages));
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
48
59
|
async exec(command) {
|
|
49
60
|
const [commandName, ...optionsList] = command.split(" ");
|
|
50
61
|
return await executeCommand(commandName, optionsList, this.path);
|
package/esm/helpers/app.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.mjs","names":[],"sources":["../../../../../../create-warlock/src/helpers/app.ts"],"sourcesContent":["import {\r\n copyDirectory,\r\n copyFile,\r\n fileExists,\r\n getFile,\r\n getJsonFile,\r\n putFile,\r\n putJsonFile,\r\n renameFile,\r\n} from \"@warlock.js/fs\";\r\nimport { existsSync, rmSync, unlinkSync } from \"node:fs\";\r\nimport path from \"path\";\r\nimport { Application, AppOptions } from \"../commands/create-new-app/types\";\r\nimport { getDatabaseDriver } from \"../features/database-drivers\";\r\nimport { executeCommand, runCommand } from \"./exec\";\r\nimport { insertImportInSortedPosition } from \"./insert-import\";\r\nimport { getPackageManager } from \"./package-manager\";\r\nimport { packageRoot, Template, template } from \"./paths\";\r\nimport { fallbackRange } from \"./warlock-versions\";\r\n\r\n/**\r\n * Environment for every install the scaffolder spawns.\r\n *\r\n * A shell that exports `NODE_ENV=production` makes the package managers skip\r\n * every devDependency — and still exit 0. The scaffolded project needs its dev\r\n * toolchain (typescript, vitest, eslint) to be usable at all, so the install\r\n * child is pinned to `development` regardless of the ambient environment. A\r\n * silently incomplete install that reports success is the exact failure mode\r\n * this file exists to prevent.\r\n */\r\nexport function installEnvironment(): NodeJS.ProcessEnv {\r\n return { NODE_ENV: \"development\" };\r\n}\r\n\r\nexport class App {\r\n /**\r\n * Resolved files\r\n */\r\n protected files: Record<string, FileManager> = {};\r\n\r\n /**\r\n * Resolved JSON files\r\n */\r\n protected jsonFiles: Record<string, JsonFileManager> = {};\r\n\r\n public isInstalled = false;\r\n\r\n public constructor(protected app: Application) {}\r\n\r\n public get options(): AppOptions {\r\n return this.app.options;\r\n }\r\n\r\n public use(templateName: Template) {\r\n copyDirectory(template(templateName), this.path);\r\n\r\n if (fileExists(this.path + \"/.env.example\")) {\r\n copyFile(this.path + \"/.env.example\", this.path + \"/.env\");\r\n }\r\n\r\n renameFile(this.path + \"/_.gitignore\", this.path + \"/.gitignore\");\r\n\r\n return this;\r\n }\r\n\r\n public init() {\r\n return this;\r\n }\r\n\r\n public terminate() {\r\n // No longer using outro, using showSuccessScreen instead\r\n }\r\n\r\n public install() {\r\n return runCommand(getPackageManager(), [\"install\"], this.path, {\r\n env: installEnvironment(),\r\n });\r\n }\r\n\r\n public async exec(command: string) {\r\n const [commandName, ...optionsList] = command.split(\" \");\r\n return await executeCommand(commandName, optionsList, this.path);\r\n }\r\n\r\n public async git() {\r\n const { initializeGitRepository } =\r\n await import(\"./project-builder-helpers\");\r\n return await initializeGitRepository(this.path);\r\n }\r\n\r\n /**\r\n * Write the project's `package.json`: the project name, the chosen package\r\n * manager, and the version of every `@warlock.js/*` dependency.\r\n *\r\n * `versions` comes from {@link resolveWarlockVersions} — versions the\r\n * registry has confirmed exist. It is optional because the fluent chain is\r\n * synchronous; without it every sibling gets the caret range floored to the\r\n * scaffolder's major, which is always satisfiable by a published release.\r\n *\r\n * What it must NEVER do again is stamp the scaffolder's own version blind:\r\n * the release tooling bumps that version on every build, published or not,\r\n * so an unverified pin resolves to nothing and the install dies with ETARGET.\r\n */\r\n public updatePackageJson(versions: Record<string, string> = {}) {\r\n const packageManager = getPackageManager();\r\n\r\n const pkg = this.package.replace(\"name\", this.name.replaceAll(\"/\", \"-\"));\r\n\r\n const content: any = pkg.content;\r\n\r\n // Substitute the chosen package manager ONLY into the fields the template\r\n // writes it into literally — the `serve` script — by path, never with a\r\n // blanket substring replace over the serialized JSON. A raw\r\n // `replaceAll(\"yarn\", pm)` rewrites every occurrence of the substring\r\n // \"yarn\" anywhere in the document, so a project named `my-yarn-app`, or any\r\n // dependency/path containing \"yarn\", is silently corrupted (`--pm=npm`\r\n // turned `my-yarn-app` into `my-npm-app`). Field-scoped rewriting is also\r\n // order-independent: the name lives in `content.name` and this token lives\r\n // in `scripts.serve`, a disjoint field that cannot collide with the name\r\n // substitution above regardless of which runs first.\r\n if (typeof content.scripts?.serve === \"string\") {\r\n content.scripts.serve = content.scripts.serve.replaceAll(\r\n \"yarn\",\r\n packageManager,\r\n );\r\n }\r\n\r\n const warlockVersion: string = (\r\n getJsonFile(packageRoot(\"package.json\")) as { version: string }\r\n ).version;\r\n const defaultRange = fallbackRange(warlockVersion);\r\n\r\n for (const field of [\"dependencies\", \"devDependencies\"] as const) {\r\n const deps = content[field] as Record<string, string> | undefined;\r\n if (!deps) continue;\r\n\r\n for (const dependency of Object.keys(deps)) {\r\n if (dependency.startsWith(\"@warlock.js/\")) {\r\n deps[dependency] = versions[dependency] ?? defaultRange;\r\n }\r\n }\r\n }\r\n\r\n pkg.save();\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Configure the chosen database driver: wire `DB_DRIVER` / `DB_PORT` into\r\n * `.env`, AND pin the driver's npm package (`mongodb` / `pg`) into the\r\n * project's `package.json` dependencies.\r\n *\r\n * The dependency is written HERE — before the base `yarn install` — so the\r\n * driver is pulled deterministically by the very first install. We do NOT\r\n * rely on the post-copy `warlock add <driver> --no-install` + separate\r\n * batched install, which can be skipped or fail and leave the driver\r\n * missing (the \"mongodb package is not installed\" runtime error).\r\n */\r\n public configureDatabaseEnv(driverValue: string) {\r\n const driver = getDatabaseDriver(driverValue);\r\n\r\n if (!driver) return this;\r\n\r\n // Pin the driver package into dependencies (idempotent — never downgrade).\r\n const packageJsonPath = path.resolve(this.path, \"package.json\");\r\n const packageJson = getJsonFile(packageJsonPath) as {\r\n dependencies?: Record<string, string>;\r\n };\r\n packageJson.dependencies = packageJson.dependencies ?? {};\r\n if (!packageJson.dependencies[driver.package]) {\r\n packageJson.dependencies[driver.package] = driver.packageVersion;\r\n putJsonFile(packageJsonPath, packageJson);\r\n }\r\n\r\n let envContent = getFile(this.path + \"/.env\") as string;\r\n\r\n envContent = envContent.replace(\r\n /DB_PORT=\\d+/,\r\n `DB_PORT=${driver.defaultPort}`,\r\n );\r\n\r\n if (envContent.includes(\"DB_DRIVER=\")) {\r\n envContent = envContent.replace(\r\n /DB_DRIVER=\\w*/,\r\n `DB_DRIVER=${driver.value}`,\r\n );\r\n } else {\r\n envContent = envContent.replace(\r\n /DB_PORT=\\d+/,\r\n `DB_PORT=${driver.defaultPort}\\nDB_DRIVER=${driver.value}`,\r\n );\r\n }\r\n\r\n putFile(this.path + \"/.env\", envContent);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Write the correct `src/config/database.ts` for the chosen driver and remove\r\n * every per-driver variant so the scaffold ships exactly one config file.\r\n *\r\n * The template ships a Mongo-shaped `database.ts` (the default) plus a\r\n * `database.postgres.ts` variant. Mongo keeps the default in place; Postgres\r\n * overwrites `database.ts` with the Postgres-shaped variant (typed\r\n * `ConnectionOptions`, `driver: \"postgres\"`, port 5432, no Mongo-only keys —\r\n * no `authSource`, `driverOptions`, `replicaSet`, `randomIncrement` /\r\n * `initialId`). Either way the `.postgres` variant file is deleted afterwards\r\n * so no leftover config ships in the generated project.\r\n *\r\n * For Postgres it also corrects the working `.env`: the Mongo template ships\r\n * `DB_AUTH=admin`, an `authSource` value Postgres ignores and its config no\r\n * longer reads, so the line is dropped to keep `.env` and the config in sync.\r\n * The port is rewritten by {@link configureDatabaseEnv} (driver defaultPort),\r\n * which runs alongside this in the same branch.\r\n */\r\n public configureDatabaseConfig(driverValue: string) {\r\n const configDir = path.resolve(this.path, \"src/config\");\r\n const databasePath = path.resolve(configDir, \"database.ts\");\r\n const postgresVariantPath = path.resolve(configDir, \"database.postgres.ts\");\r\n\r\n if (driverValue === \"postgres\" && fileExists(postgresVariantPath)) {\r\n putFile(databasePath, getFile(postgresVariantPath) as string);\r\n\r\n const envPath = this.path + \"/.env\";\r\n\r\n if (fileExists(envPath)) {\r\n const envContent = getFile(envPath) as string;\r\n // Drop the Mongo-only DB_AUTH line entirely (its whole line, so no blank\r\n // gap is left behind). Postgres has no authSource concept.\r\n putFile(envPath, envContent.replace(/^DB_AUTH=.*\\r?\\n?/m, \"\"));\r\n }\r\n }\r\n\r\n // Whatever the driver, no per-driver variant may survive into the scaffold.\r\n if (fileExists(postgresVariantPath)) {\r\n unlinkSync(postgresVariantPath);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Remove the database layer for a \"no database\" scaffold.\r\n *\r\n * Deletes `src/config/database.ts` (and a `.tsx` / per-driver variant if\r\n * present) from the freshly-copied template. The framework's database\r\n * connector is config-gated on that file — with it gone,\r\n * `config.get(\"database\")` is undefined and the connector no-ops, so the app\r\n * boots with no database wired and no driver package pulled. The `DB_*` lines\r\n * in `.env` are left in place (harmless: no config reads them) as a ready\r\n * template for adding a database back later.\r\n */\r\n public removeDatabaseConfig() {\r\n const configDir = path.resolve(this.path, \"src/config\");\r\n\r\n for (const fileName of [\r\n \"database.ts\",\r\n \"database.tsx\",\r\n \"database.postgres.ts\",\r\n ]) {\r\n const filePath = path.resolve(configDir, fileName);\r\n\r\n if (fileExists(filePath)) {\r\n unlinkSync(filePath);\r\n }\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Configure the web starter only when the web feature owns `/`.\r\n *\r\n * A dependency-free HTTP response is retained without web; otherwise the\r\n * SSR page loader owns the route.\r\n * Exactly one survives the scaffold: web projects keep the SSR page and API\r\n * example; every other project keeps the dependency-free JSON controller.\r\n */\r\n public configureWebStarter(useWeb: boolean) {\r\n const remove = (relativePath: string) => {\r\n const target = path.resolve(this.path, relativePath);\r\n if (existsSync(target)) rmSync(target, { recursive: true, force: true });\r\n };\r\n\r\n if (!useWeb) {\r\n for (const entry of [\r\n \"src/web\",\r\n \"src/app/contact\",\r\n \"src/app/locale\",\r\n \"src/shared/contact.schema.ts\",\r\n \"src/shared/locale.schema.ts\",\r\n \"src/shared/locales.ts\",\r\n \"postcss.config.mjs\",\r\n \"skills/web-code-standards\",\r\n ]) {\r\n remove(entry);\r\n }\r\n\r\n const agentsPath = path.resolve(this.path, \"AGENTS.md\");\r\n const agents = getFile(agentsPath) as string;\r\n putFile(\r\n agentsPath,\r\n agents.replace(/^.*\\*\\*web-code-standards\\*\\*.*\\r?\\n/m, \"\"),\r\n );\r\n\r\n const tsconfigPath = path.resolve(this.path, \"tsconfig.json\");\r\n const tsconfig = getFile(tsconfigPath) as string;\r\n putFile(\r\n tsconfigPath,\r\n tsconfig\r\n .replace(' \"web/*\": [\"./src/web/*\"],\\n', \"\")\r\n .replace(' \"@shared/*\": [\"./src/shared/*\"]\\n', \"\"),\r\n );\r\n\r\n return this;\r\n }\r\n\r\n // The SSR page owns `/`; its loader still uses src/app/home/services.\r\n remove(\"src/app/home/controllers/home-page.controller.ts\");\r\n remove(\"src/app/home/routes.ts\");\r\n\r\n const packageJsonPath = path.resolve(this.path, \"package.json\");\r\n const packageJson = getJsonFile(packageJsonPath) as {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n };\r\n packageJson.dependencies = {\r\n ...packageJson.dependencies,\r\n \"@mongez/http\": \"^3.5.0\",\r\n \"@mongez/react-form\": \"^4.0.0\",\r\n };\r\n packageJson.devDependencies = {\r\n ...packageJson.devDependencies,\r\n \"@tailwindcss/postcss\": \"^4.1.16\",\r\n tailwindcss: \"^4.1.16\",\r\n };\r\n putJsonFile(packageJsonPath, packageJson);\r\n\r\n const configPath = path.resolve(this.path, \"warlock.config.ts\");\r\n const config = getFile(configPath) as string;\r\n if (!config.includes('\"@warlock.js/web/connector\"')) {\r\n // Inserted in sorted position, not prepended. The generated app formats\r\n // with `prettier-plugin-organize-imports`, so an import pushed to the top\r\n // of a file that already imports `@warlock.js/auth` and `@warlock.js/core`\r\n // is out of order the moment it is written — and the developer meets it as\r\n // a `prettier/prettier` error on their first `warlock dev`, in a file they\r\n // did not write.\r\n const withConnector = insertImportInSortedPosition(\r\n config,\r\n 'import { webConnector } from \"@warlock.js/web/connector\";',\r\n \"@warlock.js/web/connector\",\r\n );\r\n\r\n putFile(\r\n configPath,\r\n withConnector.replace(\r\n \"export default defineConfig({\",\r\n \"export default defineConfig({\\n connectors: [webConnector()],\",\r\n ),\r\n );\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Record non-default `agent-kit` targets into the generated `package.json`'s\r\n * `agentKit.targets` block.\r\n *\r\n * The template's own `postinstall` (`agent-kit init && agent-kit sync`)\r\n * already targets `claude` with no flags, so the single-target default case\r\n * needs no template edit at all — this only writes when the selection is\r\n * anything else, keeping the scaffold diff-free for the common path.\r\n */\r\n public configureAgentKitTargets(agents: string[]) {\r\n const isDefault = agents.length === 1 && agents[0] === \"claude\";\r\n if (isDefault) return this;\r\n\r\n const packageJsonPath = path.resolve(this.path, \"package.json\");\r\n if (!fileExists(packageJsonPath)) return this;\r\n\r\n const packageJson = getJsonFile(packageJsonPath) as {\r\n agentKit?: { targets?: string[] };\r\n };\r\n packageJson.agentKit = { ...packageJson.agentKit, targets: agents };\r\n putJsonFile(packageJsonPath, packageJson);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Install the selected optional features by delegating to the project's own\r\n * `warlock add`. `--no-install` records every dependency in package.json and\r\n * ejects configs / scripts / setup hooks WITHOUT installing — the caller runs\r\n * one batched install afterwards. Versions come from core's feature map, so\r\n * the scaffolder never duplicates them.\r\n *\r\n * `--no-install` is passed LAST on purpose: the CLI parser treats the\r\n * positional after a bare flag as that flag's value, so it must follow the\r\n * feature list, not precede it.\r\n */\r\n public async installFeatures(features: string[]) {\r\n if (features.length === 0) return true;\r\n\r\n return this.exec(`npx warlock add ${features.join(\" \")} --no-install`);\r\n }\r\n\r\n /**\r\n * Force a single copy of `vite` when the project ends up with BOTH `vite`\r\n * (written by the `web` feature) and `vitest` (always in the template).\r\n *\r\n * `vitest` depends on vite through a wide range of its own\r\n * (`^6 || ^7 || ^8`), which resolves to a DIFFERENT major than the range the\r\n * feature map pins. Yarn 1 then has to nest the second copy under\r\n * `vitest/node_modules` and dies on its own linker invariant:\r\n *\r\n * error Invariant Violation: could not find a copy of vite to link in\r\n * <project>/node_modules/vitest/node_modules\r\n *\r\n * It aborts BEFORE `node_modules/.bin` is written, so `warlock` is never\r\n * linked and the very first `yarn dev` fails with \"'warlock' is not\r\n * recognized\". npm and pnpm nest happily and never hit this — for them the\r\n * `overrides` twin below is one copy instead of two, not a crash fix.\r\n *\r\n * The pin is READ BACK from whatever range the project's own package.json\r\n * declares, never re-stated as a literal here: the feature map in\r\n * `@warlock.js/core` owns the version, and a second copy of it would drift\r\n * the moment that one moves.\r\n *\r\n * Returns whether a pin was written, so the caller (and its spec) can assert\r\n * it instead of assuming it.\r\n */\r\n public pinViteResolution() {\r\n const packageJsonPath = path.resolve(this.path, \"package.json\");\r\n\r\n if (!fileExists(packageJsonPath)) return false;\r\n\r\n const packageJson = getJsonFile(packageJsonPath) as {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n resolutions?: Record<string, string>;\r\n overrides?: Record<string, string>;\r\n };\r\n\r\n const declaredRange = (name: string) =>\r\n packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name];\r\n\r\n const viteRange = declaredRange(\"vite\");\r\n\r\n // No vite, or no vitest to conflict with it — nothing to pin. Writing a\r\n // resolution for a package the project does not depend on would only\r\n // freeze a transitive tree nobody asked us to freeze.\r\n if (!viteRange || !declaredRange(\"vitest\")) return false;\r\n\r\n // yarn 1 / yarn berry read `resolutions`; npm 8+ and pnpm read `overrides`.\r\n packageJson.resolutions = { ...packageJson.resolutions, vite: viteRange };\r\n packageJson.overrides = { ...packageJson.overrides, vite: viteRange };\r\n\r\n putJsonFile(packageJsonPath, packageJson);\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * Get package json file\r\n */\r\n public get package() {\r\n return this.json(\"package.json\");\r\n }\r\n\r\n public updateDotEnv() {\r\n this.file(\".env\").replaceAll(\"appName\", this.name).save();\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get env file to update\r\n */\r\n public get env() {\r\n return this.file(\".env\");\r\n }\r\n\r\n public get name() {\r\n return this.app.appName;\r\n }\r\n\r\n public get path() {\r\n return this.app.appPath;\r\n }\r\n\r\n public file(relativePath: string) {\r\n const fullPath = path.resolve(this.path, relativePath);\r\n\r\n if (!this.files[fullPath]) {\r\n this.files[fullPath] = file(fullPath);\r\n }\r\n\r\n return this.files[fullPath];\r\n }\r\n\r\n public json(relativePath: string): JsonFileManager {\r\n const fullPath = path.resolve(this.path, relativePath);\r\n\r\n if (!this.jsonFiles[fullPath]) {\r\n this.jsonFiles[fullPath] = jsonFile(fullPath);\r\n }\r\n\r\n return this.jsonFiles[fullPath];\r\n }\r\n}\r\n\r\nexport function app(app: Application) {\r\n return new App(app);\r\n}\r\n\r\nexport class FileManager {\r\n public content!: string;\r\n public constructor(protected filePath: string) {\r\n this.parseContent();\r\n }\r\n\r\n protected parseContent() {\r\n this.content = getFile(this.filePath) as string;\r\n }\r\n\r\n public replace(search: string, replace: string) {\r\n this.content = this.content.replace(search, replace);\r\n\r\n return this;\r\n }\r\n\r\n public replaceAll(search: string, replace: string) {\r\n this.content = this.content.replaceAll(search, replace);\r\n\r\n return this;\r\n }\r\n\r\n public save() {\r\n putFile(this.filePath, this.content);\r\n }\r\n}\r\n\r\nexport class JsonFileManager extends FileManager {\r\n protected parseContent() {\r\n this.content = getJsonFile(this.filePath);\r\n }\r\n\r\n public save() {\r\n putJsonFile(this.filePath, this.content);\r\n }\r\n\r\n public has(key: string) {\r\n return this.content[key] !== undefined;\r\n }\r\n\r\n public replace(key: string, value: any) {\r\n this.content[key] = value;\r\n\r\n return this;\r\n }\r\n\r\n public replaceAll(key: string, value: any) {\r\n const contentAsString = JSON.stringify(this.content);\r\n\r\n this.content = JSON.parse(contentAsString.replaceAll(key, value));\r\n\r\n return this;\r\n }\r\n}\r\n\r\nexport function file(path: string) {\r\n return new FileManager(path);\r\n}\r\n\r\nexport function jsonFile(path: string) {\r\n return new JsonFileManager(path);\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"app.mjs","names":[],"sources":["../../../../../../create-warlock/src/helpers/app.ts"],"sourcesContent":["import {\r\n copyDirectory,\r\n copyFile,\r\n fileExists,\r\n getFile,\r\n getJsonFile,\r\n putFile,\r\n putJsonFile,\r\n renameFile,\r\n} from \"@warlock.js/fs\";\r\nimport { existsSync, rmSync, unlinkSync } from \"node:fs\";\r\nimport path from \"path\";\r\nimport { Application, AppOptions } from \"../commands/create-new-app/types\";\r\nimport { getDatabaseDriver } from \"../features/database-drivers\";\r\nimport { executeCommand, runCommand } from \"./exec\";\r\nimport { insertImportInSortedPosition } from \"./insert-import\";\r\nimport { getPackageManager } from \"./package-manager\";\r\nimport { packageRoot, Template, template } from \"./paths\";\r\nimport {\r\n mergePnpmBuildApprovals,\r\n requiredPnpmBuildApprovals,\r\n} from \"./pnpm-build-approvals\";\r\nimport { fallbackRange } from \"./warlock-versions\";\r\n\r\n/**\r\n * Environment for every install the scaffolder spawns.\r\n *\r\n * A shell that exports `NODE_ENV=production` makes the package managers skip\r\n * every devDependency — and still exit 0. The scaffolded project needs its dev\r\n * toolchain (typescript, vitest, eslint) to be usable at all, so the install\r\n * child is pinned to `development` regardless of the ambient environment. A\r\n * silently incomplete install that reports success is the exact failure mode\r\n * this file exists to prevent.\r\n */\r\nexport function installEnvironment(): NodeJS.ProcessEnv {\r\n return { NODE_ENV: \"development\" };\r\n}\r\n\r\nexport class App {\r\n /**\r\n * Resolved files\r\n */\r\n protected files: Record<string, FileManager> = {};\r\n\r\n /**\r\n * Resolved JSON files\r\n */\r\n protected jsonFiles: Record<string, JsonFileManager> = {};\r\n\r\n public isInstalled = false;\r\n\r\n public constructor(protected app: Application) {}\r\n\r\n public get options(): AppOptions {\r\n return this.app.options;\r\n }\r\n\r\n public use(templateName: Template) {\r\n copyDirectory(template(templateName), this.path);\r\n\r\n if (fileExists(this.path + \"/.env.example\")) {\r\n copyFile(this.path + \"/.env.example\", this.path + \"/.env\");\r\n }\r\n\r\n renameFile(this.path + \"/_.gitignore\", this.path + \"/.gitignore\");\r\n\r\n return this;\r\n }\r\n\r\n public init() {\r\n return this;\r\n }\r\n\r\n public terminate() {\r\n // No longer using outro, using showSuccessScreen instead\r\n }\r\n\r\n public install() {\r\n return runCommand(getPackageManager(), [\"install\"], this.path, {\r\n env: installEnvironment(),\r\n });\r\n }\r\n\r\n /** Record native build approvals before pnpm installs selected feature dependencies. */\r\n public approveSelectedFeatureBuilds(features: readonly string[]) {\r\n if (getPackageManager() !== \"pnpm\") return this;\r\n const packages = requiredPnpmBuildApprovals(features);\r\n if (packages.length === 0) return this;\r\n const workspacePath = path.join(this.path, \"pnpm-workspace.yaml\");\r\n if (!fileExists(workspacePath)) return this;\r\n const current = getFile(workspacePath) as string;\r\n putFile(workspacePath, mergePnpmBuildApprovals(current, packages));\r\n return this;\r\n }\r\n\r\n public async exec(command: string) {\r\n const [commandName, ...optionsList] = command.split(\" \");\r\n return await executeCommand(commandName, optionsList, this.path);\r\n }\r\n\r\n public async git() {\r\n const { initializeGitRepository } =\r\n await import(\"./project-builder-helpers\");\r\n return await initializeGitRepository(this.path);\r\n }\r\n\r\n /**\r\n * Write the project's `package.json`: the project name, the chosen package\r\n * manager, and the version of every `@warlock.js/*` dependency.\r\n *\r\n * `versions` comes from {@link resolveWarlockVersions} — versions the\r\n * registry has confirmed exist. It is optional because the fluent chain is\r\n * synchronous; without it every sibling gets the caret range floored to the\r\n * scaffolder's major, which is always satisfiable by a published release.\r\n *\r\n * What it must NEVER do again is stamp the scaffolder's own version blind:\r\n * the release tooling bumps that version on every build, published or not,\r\n * so an unverified pin resolves to nothing and the install dies with ETARGET.\r\n */\r\n public updatePackageJson(versions: Record<string, string> = {}) {\r\n const packageManager = getPackageManager();\r\n\r\n const pkg = this.package.replace(\"name\", this.name.replaceAll(\"/\", \"-\"));\r\n\r\n const content: any = pkg.content;\r\n\r\n // Substitute the chosen package manager ONLY into the fields the template\r\n // writes it into literally — the `serve` script — by path, never with a\r\n // blanket substring replace over the serialized JSON. A raw\r\n // `replaceAll(\"yarn\", pm)` rewrites every occurrence of the substring\r\n // \"yarn\" anywhere in the document, so a project named `my-yarn-app`, or any\r\n // dependency/path containing \"yarn\", is silently corrupted (`--pm=npm`\r\n // turned `my-yarn-app` into `my-npm-app`). Field-scoped rewriting is also\r\n // order-independent: the name lives in `content.name` and this token lives\r\n // in `scripts.serve`, a disjoint field that cannot collide with the name\r\n // substitution above regardless of which runs first.\r\n if (typeof content.scripts?.serve === \"string\") {\r\n content.scripts.serve = content.scripts.serve.replaceAll(\r\n \"yarn\",\r\n packageManager,\r\n );\r\n }\r\n\r\n const warlockVersion: string = (\r\n getJsonFile(packageRoot(\"package.json\")) as { version: string }\r\n ).version;\r\n const defaultRange = fallbackRange(warlockVersion);\r\n\r\n for (const field of [\"dependencies\", \"devDependencies\"] as const) {\r\n const deps = content[field] as Record<string, string> | undefined;\r\n if (!deps) continue;\r\n\r\n for (const dependency of Object.keys(deps)) {\r\n if (dependency.startsWith(\"@warlock.js/\")) {\r\n deps[dependency] = versions[dependency] ?? defaultRange;\r\n }\r\n }\r\n }\r\n\r\n pkg.save();\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Configure the chosen database driver: wire `DB_DRIVER` / `DB_PORT` into\r\n * `.env`, AND pin the driver's npm package (`mongodb` / `pg`) into the\r\n * project's `package.json` dependencies.\r\n *\r\n * The dependency is written HERE — before the base `yarn install` — so the\r\n * driver is pulled deterministically by the very first install. We do NOT\r\n * rely on the post-copy `warlock add <driver> --no-install` + separate\r\n * batched install, which can be skipped or fail and leave the driver\r\n * missing (the \"mongodb package is not installed\" runtime error).\r\n */\r\n public configureDatabaseEnv(driverValue: string) {\r\n const driver = getDatabaseDriver(driverValue);\r\n\r\n if (!driver) return this;\r\n\r\n // Pin the driver package into dependencies (idempotent — never downgrade).\r\n const packageJsonPath = path.resolve(this.path, \"package.json\");\r\n const packageJson = getJsonFile(packageJsonPath) as {\r\n dependencies?: Record<string, string>;\r\n };\r\n packageJson.dependencies = packageJson.dependencies ?? {};\r\n if (!packageJson.dependencies[driver.package]) {\r\n packageJson.dependencies[driver.package] = driver.packageVersion;\r\n putJsonFile(packageJsonPath, packageJson);\r\n }\r\n\r\n let envContent = getFile(this.path + \"/.env\") as string;\r\n\r\n envContent = envContent.replace(\r\n /DB_PORT=\\d+/,\r\n `DB_PORT=${driver.defaultPort}`,\r\n );\r\n\r\n if (envContent.includes(\"DB_DRIVER=\")) {\r\n envContent = envContent.replace(\r\n /DB_DRIVER=\\w*/,\r\n `DB_DRIVER=${driver.value}`,\r\n );\r\n } else {\r\n envContent = envContent.replace(\r\n /DB_PORT=\\d+/,\r\n `DB_PORT=${driver.defaultPort}\\nDB_DRIVER=${driver.value}`,\r\n );\r\n }\r\n\r\n putFile(this.path + \"/.env\", envContent);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Write the correct `src/config/database.ts` for the chosen driver and remove\r\n * every per-driver variant so the scaffold ships exactly one config file.\r\n *\r\n * The template ships a Mongo-shaped `database.ts` (the default) plus a\r\n * `database.postgres.ts` variant. Mongo keeps the default in place; Postgres\r\n * overwrites `database.ts` with the Postgres-shaped variant (typed\r\n * `ConnectionOptions`, `driver: \"postgres\"`, port 5432, no Mongo-only keys —\r\n * no `authSource`, `driverOptions`, `replicaSet`, `randomIncrement` /\r\n * `initialId`). Either way the `.postgres` variant file is deleted afterwards\r\n * so no leftover config ships in the generated project.\r\n *\r\n * For Postgres it also corrects the working `.env`: the Mongo template ships\r\n * `DB_AUTH=admin`, an `authSource` value Postgres ignores and its config no\r\n * longer reads, so the line is dropped to keep `.env` and the config in sync.\r\n * The port is rewritten by {@link configureDatabaseEnv} (driver defaultPort),\r\n * which runs alongside this in the same branch.\r\n */\r\n public configureDatabaseConfig(driverValue: string) {\r\n const configDir = path.resolve(this.path, \"src/config\");\r\n const databasePath = path.resolve(configDir, \"database.ts\");\r\n const postgresVariantPath = path.resolve(configDir, \"database.postgres.ts\");\r\n\r\n if (driverValue === \"postgres\" && fileExists(postgresVariantPath)) {\r\n putFile(databasePath, getFile(postgresVariantPath) as string);\r\n\r\n const envPath = this.path + \"/.env\";\r\n\r\n if (fileExists(envPath)) {\r\n const envContent = getFile(envPath) as string;\r\n // Drop the Mongo-only DB_AUTH line entirely (its whole line, so no blank\r\n // gap is left behind). Postgres has no authSource concept.\r\n putFile(envPath, envContent.replace(/^DB_AUTH=.*\\r?\\n?/m, \"\"));\r\n }\r\n }\r\n\r\n // Whatever the driver, no per-driver variant may survive into the scaffold.\r\n if (fileExists(postgresVariantPath)) {\r\n unlinkSync(postgresVariantPath);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Remove the database layer for a \"no database\" scaffold.\r\n *\r\n * Deletes `src/config/database.ts` (and a `.tsx` / per-driver variant if\r\n * present) from the freshly-copied template. The framework's database\r\n * connector is config-gated on that file — with it gone,\r\n * `config.get(\"database\")` is undefined and the connector no-ops, so the app\r\n * boots with no database wired and no driver package pulled. The `DB_*` lines\r\n * in `.env` are left in place (harmless: no config reads them) as a ready\r\n * template for adding a database back later.\r\n */\r\n public removeDatabaseConfig() {\r\n const configDir = path.resolve(this.path, \"src/config\");\r\n\r\n for (const fileName of [\r\n \"database.ts\",\r\n \"database.tsx\",\r\n \"database.postgres.ts\",\r\n ]) {\r\n const filePath = path.resolve(configDir, fileName);\r\n\r\n if (fileExists(filePath)) {\r\n unlinkSync(filePath);\r\n }\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Configure the web starter only when the web feature owns `/`.\r\n *\r\n * A dependency-free HTTP response is retained without web; otherwise the\r\n * SSR page loader owns the route.\r\n * Exactly one survives the scaffold: web projects keep the SSR page and API\r\n * example; every other project keeps the dependency-free JSON controller.\r\n */\r\n public configureWebStarter(useWeb: boolean) {\r\n const remove = (relativePath: string) => {\r\n const target = path.resolve(this.path, relativePath);\r\n if (existsSync(target)) rmSync(target, { recursive: true, force: true });\r\n };\r\n\r\n if (!useWeb) {\r\n for (const entry of [\r\n \"src/web\",\r\n \"src/app/contact\",\r\n \"src/app/locale\",\r\n \"src/shared/contact.schema.ts\",\r\n \"src/shared/locale.schema.ts\",\r\n \"src/shared/locales.ts\",\r\n \"postcss.config.mjs\",\r\n \"skills/web-code-standards\",\r\n ]) {\r\n remove(entry);\r\n }\r\n\r\n const agentsPath = path.resolve(this.path, \"AGENTS.md\");\r\n const agents = getFile(agentsPath) as string;\r\n putFile(\r\n agentsPath,\r\n agents.replace(/^.*\\*\\*web-code-standards\\*\\*.*\\r?\\n/m, \"\"),\r\n );\r\n\r\n const tsconfigPath = path.resolve(this.path, \"tsconfig.json\");\r\n const tsconfig = getFile(tsconfigPath) as string;\r\n putFile(\r\n tsconfigPath,\r\n tsconfig\r\n .replace(' \"web/*\": [\"./src/web/*\"],\\n', \"\")\r\n .replace(' \"@shared/*\": [\"./src/shared/*\"]\\n', \"\"),\r\n );\r\n\r\n return this;\r\n }\r\n\r\n // The SSR page owns `/`; its loader still uses src/app/home/services.\r\n remove(\"src/app/home/controllers/home-page.controller.ts\");\r\n remove(\"src/app/home/routes.ts\");\r\n\r\n const packageJsonPath = path.resolve(this.path, \"package.json\");\r\n const packageJson = getJsonFile(packageJsonPath) as {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n };\r\n packageJson.dependencies = {\r\n ...packageJson.dependencies,\r\n \"@mongez/http\": \"^3.5.0\",\r\n \"@mongez/react-form\": \"^4.0.0\",\r\n };\r\n packageJson.devDependencies = {\r\n ...packageJson.devDependencies,\r\n \"@tailwindcss/postcss\": \"^4.1.16\",\r\n tailwindcss: \"^4.1.16\",\r\n };\r\n putJsonFile(packageJsonPath, packageJson);\r\n\r\n const configPath = path.resolve(this.path, \"warlock.config.ts\");\r\n const config = getFile(configPath) as string;\r\n if (!config.includes('\"@warlock.js/web/connector\"')) {\r\n // Inserted in sorted position, not prepended. The generated app formats\r\n // with `prettier-plugin-organize-imports`, so an import pushed to the top\r\n // of a file that already imports `@warlock.js/auth` and `@warlock.js/core`\r\n // is out of order the moment it is written — and the developer meets it as\r\n // a `prettier/prettier` error on their first `warlock dev`, in a file they\r\n // did not write.\r\n const withConnector = insertImportInSortedPosition(\r\n config,\r\n 'import { webConnector } from \"@warlock.js/web/connector\";',\r\n \"@warlock.js/web/connector\",\r\n );\r\n\r\n putFile(\r\n configPath,\r\n withConnector.replace(\r\n \"export default defineConfig({\",\r\n \"export default defineConfig({\\n connectors: [webConnector()],\",\r\n ),\r\n );\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Record non-default `agent-kit` targets into the generated `package.json`'s\r\n * `agentKit.targets` block.\r\n *\r\n * The template's own `postinstall` (`agent-kit init && agent-kit sync`)\r\n * already targets `claude` with no flags, so the single-target default case\r\n * needs no template edit at all — this only writes when the selection is\r\n * anything else, keeping the scaffold diff-free for the common path.\r\n */\r\n public configureAgentKitTargets(agents: string[]) {\r\n const isDefault = agents.length === 1 && agents[0] === \"claude\";\r\n if (isDefault) return this;\r\n\r\n const packageJsonPath = path.resolve(this.path, \"package.json\");\r\n if (!fileExists(packageJsonPath)) return this;\r\n\r\n const packageJson = getJsonFile(packageJsonPath) as {\r\n agentKit?: { targets?: string[] };\r\n };\r\n packageJson.agentKit = { ...packageJson.agentKit, targets: agents };\r\n putJsonFile(packageJsonPath, packageJson);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Install the selected optional features by delegating to the project's own\r\n * `warlock add`. `--no-install` records every dependency in package.json and\r\n * ejects configs / scripts / setup hooks WITHOUT installing — the caller runs\r\n * one batched install afterwards. Versions come from core's feature map, so\r\n * the scaffolder never duplicates them.\r\n *\r\n * `--no-install` is passed LAST on purpose: the CLI parser treats the\r\n * positional after a bare flag as that flag's value, so it must follow the\r\n * feature list, not precede it.\r\n */\r\n public async installFeatures(features: string[]) {\r\n if (features.length === 0) return true;\r\n\r\n return this.exec(`npx warlock add ${features.join(\" \")} --no-install`);\r\n }\r\n\r\n /**\r\n * Force a single copy of `vite` when the project ends up with BOTH `vite`\r\n * (written by the `web` feature) and `vitest` (always in the template).\r\n *\r\n * `vitest` depends on vite through a wide range of its own\r\n * (`^6 || ^7 || ^8`), which resolves to a DIFFERENT major than the range the\r\n * feature map pins. Yarn 1 then has to nest the second copy under\r\n * `vitest/node_modules` and dies on its own linker invariant:\r\n *\r\n * error Invariant Violation: could not find a copy of vite to link in\r\n * <project>/node_modules/vitest/node_modules\r\n *\r\n * It aborts BEFORE `node_modules/.bin` is written, so `warlock` is never\r\n * linked and the very first `yarn dev` fails with \"'warlock' is not\r\n * recognized\". npm and pnpm nest happily and never hit this — for them the\r\n * `overrides` twin below is one copy instead of two, not a crash fix.\r\n *\r\n * The pin is READ BACK from whatever range the project's own package.json\r\n * declares, never re-stated as a literal here: the feature map in\r\n * `@warlock.js/core` owns the version, and a second copy of it would drift\r\n * the moment that one moves.\r\n *\r\n * Returns whether a pin was written, so the caller (and its spec) can assert\r\n * it instead of assuming it.\r\n */\r\n public pinViteResolution() {\r\n const packageJsonPath = path.resolve(this.path, \"package.json\");\r\n\r\n if (!fileExists(packageJsonPath)) return false;\r\n\r\n const packageJson = getJsonFile(packageJsonPath) as {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n resolutions?: Record<string, string>;\r\n overrides?: Record<string, string>;\r\n };\r\n\r\n const declaredRange = (name: string) =>\r\n packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name];\r\n\r\n const viteRange = declaredRange(\"vite\");\r\n\r\n // No vite, or no vitest to conflict with it — nothing to pin. Writing a\r\n // resolution for a package the project does not depend on would only\r\n // freeze a transitive tree nobody asked us to freeze.\r\n if (!viteRange || !declaredRange(\"vitest\")) return false;\r\n\r\n // yarn 1 / yarn berry read `resolutions`; npm 8+ and pnpm read `overrides`.\r\n packageJson.resolutions = { ...packageJson.resolutions, vite: viteRange };\r\n packageJson.overrides = { ...packageJson.overrides, vite: viteRange };\r\n\r\n putJsonFile(packageJsonPath, packageJson);\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * Get package json file\r\n */\r\n public get package() {\r\n return this.json(\"package.json\");\r\n }\r\n\r\n public updateDotEnv() {\r\n this.file(\".env\").replaceAll(\"appName\", this.name).save();\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get env file to update\r\n */\r\n public get env() {\r\n return this.file(\".env\");\r\n }\r\n\r\n public get name() {\r\n return this.app.appName;\r\n }\r\n\r\n public get path() {\r\n return this.app.appPath;\r\n }\r\n\r\n public file(relativePath: string) {\r\n const fullPath = path.resolve(this.path, relativePath);\r\n\r\n if (!this.files[fullPath]) {\r\n this.files[fullPath] = file(fullPath);\r\n }\r\n\r\n return this.files[fullPath];\r\n }\r\n\r\n public json(relativePath: string): JsonFileManager {\r\n const fullPath = path.resolve(this.path, relativePath);\r\n\r\n if (!this.jsonFiles[fullPath]) {\r\n this.jsonFiles[fullPath] = jsonFile(fullPath);\r\n }\r\n\r\n return this.jsonFiles[fullPath];\r\n }\r\n}\r\n\r\nexport function app(app: Application) {\r\n return new App(app);\r\n}\r\n\r\nexport class FileManager {\r\n public content!: string;\r\n public constructor(protected filePath: string) {\r\n this.parseContent();\r\n }\r\n\r\n protected parseContent() {\r\n this.content = getFile(this.filePath) as string;\r\n }\r\n\r\n public replace(search: string, replace: string) {\r\n this.content = this.content.replace(search, replace);\r\n\r\n return this;\r\n }\r\n\r\n public replaceAll(search: string, replace: string) {\r\n this.content = this.content.replaceAll(search, replace);\r\n\r\n return this;\r\n }\r\n\r\n public save() {\r\n putFile(this.filePath, this.content);\r\n }\r\n}\r\n\r\nexport class JsonFileManager extends FileManager {\r\n protected parseContent() {\r\n this.content = getJsonFile(this.filePath);\r\n }\r\n\r\n public save() {\r\n putJsonFile(this.filePath, this.content);\r\n }\r\n\r\n public has(key: string) {\r\n return this.content[key] !== undefined;\r\n }\r\n\r\n public replace(key: string, value: any) {\r\n this.content[key] = value;\r\n\r\n return this;\r\n }\r\n\r\n public replaceAll(key: string, value: any) {\r\n const contentAsString = JSON.stringify(this.content);\r\n\r\n this.content = JSON.parse(contentAsString.replaceAll(key, value));\r\n\r\n return this;\r\n }\r\n}\r\n\r\nexport function file(path: string) {\r\n return new FileManager(path);\r\n}\r\n\r\nexport function jsonFile(path: string) {\r\n return new JsonFileManager(path);\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,qBAAwC;CACtD,OAAO,EAAE,UAAU,cAAc;AACnC;AAEA,IAAa,MAAb,MAAiB;CAaf,AAAO,YAAY,AAAU,KAAkB;EAAlB;eATkB,CAAC;mBAKO,CAAC;qBAEnC;CAE2B;CAEhD,IAAW,UAAsB;EAC/B,OAAO,KAAK,IAAI;CAClB;CAEA,AAAO,IAAI,cAAwB;EACjC,cAAc,SAAS,YAAY,GAAG,KAAK,IAAI;EAE/C,IAAI,WAAW,KAAK,OAAO,eAAe,GACxC,SAAS,KAAK,OAAO,iBAAiB,KAAK,OAAO,OAAO;EAG3D,WAAW,KAAK,OAAO,gBAAgB,KAAK,OAAO,aAAa;EAEhE,OAAO;CACT;CAEA,AAAO,OAAO;EACZ,OAAO;CACT;CAEA,AAAO,YAAY,CAEnB;CAEA,AAAO,UAAU;EACf,OAAO,WAAW,kBAAkB,GAAG,CAAC,SAAS,GAAG,KAAK,MAAM,EAC7D,KAAK,mBAAmB,EAC1B,CAAC;CACH;;CAGA,AAAO,6BAA6B,UAA6B;EAC/D,IAAI,kBAAkB,MAAM,QAAQ,OAAO;EAC3C,MAAM,WAAW,2BAA2B,QAAQ;EACpD,IAAI,SAAS,WAAW,GAAG,OAAO;EAClC,MAAM,gBAAgB,KAAK,KAAK,KAAK,MAAM,qBAAqB;EAChE,IAAI,CAAC,WAAW,aAAa,GAAG,OAAO;EAEvC,QAAQ,eAAe,wBADP,QAAQ,aAC6B,GAAG,QAAQ,CAAC;EACjE,OAAO;CACT;CAEA,MAAa,KAAK,SAAiB;EACjC,MAAM,CAAC,aAAa,GAAG,eAAe,QAAQ,MAAM,GAAG;EACvD,OAAO,MAAM,eAAe,aAAa,aAAa,KAAK,IAAI;CACjE;CAEA,MAAa,MAAM;EACjB,MAAM,EAAE,4BACN,MAAM,OAAO;EACf,OAAO,MAAM,wBAAwB,KAAK,IAAI;CAChD;;;;;;;;;;;;;;CAeA,AAAO,kBAAkB,WAAmC,CAAC,GAAG;EAC9D,MAAM,iBAAiB,kBAAkB;EAEzC,MAAM,MAAM,KAAK,QAAQ,QAAQ,QAAQ,KAAK,KAAK,WAAW,KAAK,GAAG,CAAC;EAEvE,MAAM,UAAe,IAAI;EAYzB,IAAI,OAAO,QAAQ,SAAS,UAAU,UACpC,QAAQ,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,WAC5C,QACA,cACF;EAGF,MAAM,iBACJ,YAAY,YAAY,cAAc,CAAC,CAAC,CACxC;EACF,MAAM,eAAe,cAAc,cAAc;EAEjD,KAAK,MAAM,SAAS,CAAC,gBAAgB,iBAAiB,GAAY;GAChE,MAAM,OAAO,QAAQ;GACrB,IAAI,CAAC,MAAM;GAEX,KAAK,MAAM,cAAc,OAAO,KAAK,IAAI,GACvC,IAAI,WAAW,WAAW,cAAc,GACtC,KAAK,cAAc,SAAS,eAAe;EAGjD;EAEA,IAAI,KAAK;EAET,OAAO;CACT;;;;;;;;;;;;CAaA,AAAO,qBAAqB,aAAqB;EAC/C,MAAM,SAAS,kBAAkB,WAAW;EAE5C,IAAI,CAAC,QAAQ,OAAO;EAGpB,MAAM,kBAAkB,KAAK,QAAQ,KAAK,MAAM,cAAc;EAC9D,MAAM,cAAc,YAAY,eAAe;EAG/C,YAAY,eAAe,YAAY,gBAAgB,CAAC;EACxD,IAAI,CAAC,YAAY,aAAa,OAAO,UAAU;GAC7C,YAAY,aAAa,OAAO,WAAW,OAAO;GAClD,YAAY,iBAAiB,WAAW;EAC1C;EAEA,IAAI,aAAa,QAAQ,KAAK,OAAO,OAAO;EAE5C,aAAa,WAAW,QACtB,eACA,WAAW,OAAO,aACpB;EAEA,IAAI,WAAW,SAAS,YAAY,GAClC,aAAa,WAAW,QACtB,iBACA,aAAa,OAAO,OACtB;OAEA,aAAa,WAAW,QACtB,eACA,WAAW,OAAO,YAAY,cAAc,OAAO,OACrD;EAGF,QAAQ,KAAK,OAAO,SAAS,UAAU;EAEvC,OAAO;CACT;;;;;;;;;;;;;;;;;;;CAoBA,AAAO,wBAAwB,aAAqB;EAClD,MAAM,YAAY,KAAK,QAAQ,KAAK,MAAM,YAAY;EACtD,MAAM,eAAe,KAAK,QAAQ,WAAW,aAAa;EAC1D,MAAM,sBAAsB,KAAK,QAAQ,WAAW,sBAAsB;EAE1E,IAAI,gBAAgB,cAAc,WAAW,mBAAmB,GAAG;GACjE,QAAQ,cAAc,QAAQ,mBAAmB,CAAW;GAE5D,MAAM,UAAU,KAAK,OAAO;GAE5B,IAAI,WAAW,OAAO,GAIpB,QAAQ,SAHW,QAAQ,OAGD,CAAC,CAAC,QAAQ,sBAAsB,EAAE,CAAC;EAEjE;EAGA,IAAI,WAAW,mBAAmB,GAChC,WAAW,mBAAmB;EAGhC,OAAO;CACT;;;;;;;;;;;;CAaA,AAAO,uBAAuB;EAC5B,MAAM,YAAY,KAAK,QAAQ,KAAK,MAAM,YAAY;EAEtD,KAAK,MAAM,YAAY;GACrB;GACA;GACA;EACF,GAAG;GACD,MAAM,WAAW,KAAK,QAAQ,WAAW,QAAQ;GAEjD,IAAI,WAAW,QAAQ,GACrB,WAAW,QAAQ;EAEvB;EAEA,OAAO;CACT;;;;;;;;;CAUA,AAAO,oBAAoB,QAAiB;EAC1C,MAAM,UAAU,iBAAyB;GACvC,MAAM,SAAS,KAAK,QAAQ,KAAK,MAAM,YAAY;GACnD,IAAI,WAAW,MAAM,GAAG,OAAO,QAAQ;IAAE,WAAW;IAAM,OAAO;GAAK,CAAC;EACzE;EAEA,IAAI,CAAC,QAAQ;GACX,KAAK,MAAM,SAAS;IAClB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF,GACE,OAAO,KAAK;GAGd,MAAM,aAAa,KAAK,QAAQ,KAAK,MAAM,WAAW;GAEtD,QACE,YAFa,QAAQ,UAGhB,CAAC,CAAC,QAAQ,yCAAyC,EAAE,CAC5D;GAEA,MAAM,eAAe,KAAK,QAAQ,KAAK,MAAM,eAAe;GAE5D,QACE,cAFe,QAAQ,YAGhB,CAAC,CACL,QAAQ,yCAAqC,EAAE,CAAC,CAChD,QAAQ,+CAA2C,EAAE,CAC1D;GAEA,OAAO;EACT;EAGA,OAAO,kDAAkD;EACzD,OAAO,wBAAwB;EAE/B,MAAM,kBAAkB,KAAK,QAAQ,KAAK,MAAM,cAAc;EAC9D,MAAM,cAAc,YAAY,eAAe;EAI/C,YAAY,eAAe;GACzB,GAAG,YAAY;GACf,gBAAgB;GAChB,sBAAsB;EACxB;EACA,YAAY,kBAAkB;GAC5B,GAAG,YAAY;GACf,wBAAwB;GACxB,aAAa;EACf;EACA,YAAY,iBAAiB,WAAW;EAExC,MAAM,aAAa,KAAK,QAAQ,KAAK,MAAM,mBAAmB;EAC9D,MAAM,SAAS,QAAQ,UAAU;EACjC,IAAI,CAAC,OAAO,SAAS,+BAA6B,GAahD,QACE,YAPoB,6BACpB,QACA,+DACA,2BAKY,CAAC,CAAC,QACZ,iCACA,gEACF,CACF;EAGF,OAAO;CACT;;;;;;;;;;CAWA,AAAO,yBAAyB,QAAkB;EAEhD,IADkB,OAAO,WAAW,KAAK,OAAO,OAAO,UACxC,OAAO;EAEtB,MAAM,kBAAkB,KAAK,QAAQ,KAAK,MAAM,cAAc;EAC9D,IAAI,CAAC,WAAW,eAAe,GAAG,OAAO;EAEzC,MAAM,cAAc,YAAY,eAAe;EAG/C,YAAY,WAAW;GAAE,GAAG,YAAY;GAAU,SAAS;EAAO;EAClE,YAAY,iBAAiB,WAAW;EAExC,OAAO;CACT;;;;;;;;;;;;CAaA,MAAa,gBAAgB,UAAoB;EAC/C,IAAI,SAAS,WAAW,GAAG,OAAO;EAElC,OAAO,KAAK,KAAK,mBAAmB,SAAS,KAAK,GAAG,EAAE,cAAc;CACvE;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BA,AAAO,oBAAoB;EACzB,MAAM,kBAAkB,KAAK,QAAQ,KAAK,MAAM,cAAc;EAE9D,IAAI,CAAC,WAAW,eAAe,GAAG,OAAO;EAEzC,MAAM,cAAc,YAAY,eAAe;EAO/C,MAAM,iBAAiB,SACrB,YAAY,eAAe,SAAS,YAAY,kBAAkB;EAEpE,MAAM,YAAY,cAAc,MAAM;EAKtC,IAAI,CAAC,aAAa,CAAC,cAAc,QAAQ,GAAG,OAAO;EAGnD,YAAY,cAAc;GAAE,GAAG,YAAY;GAAa,MAAM;EAAU;EACxE,YAAY,YAAY;GAAE,GAAG,YAAY;GAAW,MAAM;EAAU;EAEpE,YAAY,iBAAiB,WAAW;EAExC,OAAO;CACT;;;;CAKA,IAAW,UAAU;EACnB,OAAO,KAAK,KAAK,cAAc;CACjC;CAEA,AAAO,eAAe;EACpB,KAAK,KAAK,MAAM,CAAC,CAAC,WAAW,WAAW,KAAK,IAAI,CAAC,CAAC,KAAK;EAExD,OAAO;CACT;;;;CAKA,IAAW,MAAM;EACf,OAAO,KAAK,KAAK,MAAM;CACzB;CAEA,IAAW,OAAO;EAChB,OAAO,KAAK,IAAI;CAClB;CAEA,IAAW,OAAO;EAChB,OAAO,KAAK,IAAI;CAClB;CAEA,AAAO,KAAK,cAAsB;EAChC,MAAM,WAAW,KAAK,QAAQ,KAAK,MAAM,YAAY;EAErD,IAAI,CAAC,KAAK,MAAM,WACd,KAAK,MAAM,YAAY,KAAK,QAAQ;EAGtC,OAAO,KAAK,MAAM;CACpB;CAEA,AAAO,KAAK,cAAuC;EACjD,MAAM,WAAW,KAAK,QAAQ,KAAK,MAAM,YAAY;EAErD,IAAI,CAAC,KAAK,UAAU,WAClB,KAAK,UAAU,YAAY,SAAS,QAAQ;EAG9C,OAAO,KAAK,UAAU;CACxB;AACF;AAMA,IAAa,cAAb,MAAyB;CAEvB,AAAO,YAAY,AAAU,UAAkB;EAAlB;EAC3B,KAAK,aAAa;CACpB;CAEA,AAAU,eAAe;EACvB,KAAK,UAAU,QAAQ,KAAK,QAAQ;CACtC;CAEA,AAAO,QAAQ,QAAgB,SAAiB;EAC9C,KAAK,UAAU,KAAK,QAAQ,QAAQ,QAAQ,OAAO;EAEnD,OAAO;CACT;CAEA,AAAO,WAAW,QAAgB,SAAiB;EACjD,KAAK,UAAU,KAAK,QAAQ,WAAW,QAAQ,OAAO;EAEtD,OAAO;CACT;CAEA,AAAO,OAAO;EACZ,QAAQ,KAAK,UAAU,KAAK,OAAO;CACrC;AACF;AAEA,IAAa,kBAAb,cAAqC,YAAY;CAC/C,AAAU,eAAe;EACvB,KAAK,UAAU,YAAY,KAAK,QAAQ;CAC1C;CAEA,AAAO,OAAO;EACZ,YAAY,KAAK,UAAU,KAAK,OAAO;CACzC;CAEA,AAAO,IAAI,KAAa;EACtB,OAAO,KAAK,QAAQ,SAAS;CAC/B;CAEA,AAAO,QAAQ,KAAa,OAAY;EACtC,KAAK,QAAQ,OAAO;EAEpB,OAAO;CACT;CAEA,AAAO,WAAW,KAAa,OAAY;EACzC,MAAM,kBAAkB,KAAK,UAAU,KAAK,OAAO;EAEnD,KAAK,UAAU,KAAK,MAAM,gBAAgB,WAAW,KAAK,KAAK,CAAC;EAEhE,OAAO;CACT;AACF;AAEA,SAAgB,KAAK,MAAc;CACjC,OAAO,IAAI,YAAY,IAAI;AAC7B;AAEA,SAAgB,SAAS,MAAc;CACrC,OAAO,IAAI,gBAAgB,IAAI;AACjC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region ../create-warlock/src/helpers/pnpm-build-approvals.ts
|
|
2
|
+
/** Native dependency build scripts required by features the scaffold explicitly selected. */
|
|
3
|
+
const FEATURE_BUILD_APPROVALS = { image: ["sharp"] };
|
|
4
|
+
function requiredPnpmBuildApprovals(features) {
|
|
5
|
+
return [...new Set(features.flatMap((feature) => FEATURE_BUILD_APPROVALS[feature] ?? []))];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Add only known feature-required builds to pnpm 11's workspace policy.
|
|
9
|
+
* Existing `false` values are user decisions and are never overridden.
|
|
10
|
+
*/
|
|
11
|
+
function mergePnpmBuildApprovals(yaml, packages) {
|
|
12
|
+
let result = yaml;
|
|
13
|
+
for (const packageName of packages) result = mergeOneApproval(result, packageName);
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
const quotedKey = (name) => `(?:${name}|"${name}"|'${name}')`;
|
|
17
|
+
/** Edit one ordinary YAML mapping while preserving every untouched byte. */
|
|
18
|
+
function mergeOneApproval(yaml, packageName) {
|
|
19
|
+
const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20
|
+
const block = /^allowBuilds:\s*(?:#.*)?(?:\r?\n|$)/m.exec(yaml);
|
|
21
|
+
if (!block) {
|
|
22
|
+
if (/^(?:allowBuilds|"allowBuilds"|'allowBuilds')\s*:/m.test(yaml)) return yaml;
|
|
23
|
+
return `${yaml.replace(/\s*$/, "")}\nallowBuilds:\n ${packageName}: true\n`;
|
|
24
|
+
}
|
|
25
|
+
const start = block.index + block[0].length;
|
|
26
|
+
const tail = yaml.slice(start);
|
|
27
|
+
const nextTopLevel = /^(?!\s|#|$)[^\r\n]+/m.exec(tail);
|
|
28
|
+
const end = nextTopLevel ? start + nextTopLevel.index : yaml.length;
|
|
29
|
+
const mapping = yaml.slice(start, end);
|
|
30
|
+
const key = quotedKey(escaped);
|
|
31
|
+
const entry = new RegExp(`^(\\s*)${key}\\s*:\\s*([^#\\r\\n]*)(.*)$`, "m").exec(mapping);
|
|
32
|
+
if (entry) {
|
|
33
|
+
const value = entry[2].trim();
|
|
34
|
+
if (value === "true" || value === "false") return yaml;
|
|
35
|
+
if (!/^(?:set this to true or false|"set this to true or false"|'set this to true or false')$/.test(value)) return yaml;
|
|
36
|
+
const comment = entry[3] ?? "";
|
|
37
|
+
const replacement = `${entry[1]}${packageName}: true${comment.startsWith("#") ? " " : ""}${comment}`;
|
|
38
|
+
return `${yaml.slice(0, start)}${mapping.slice(0, entry.index)}${replacement}${mapping.slice(entry.index + entry[0].length)}${yaml.slice(end)}`;
|
|
39
|
+
}
|
|
40
|
+
const indentation = /^([ \t]+)\S/m.exec(mapping)?.[1] ?? " ";
|
|
41
|
+
return `${yaml.slice(0, start)}${indentation}${packageName}: true\n${mapping}${yaml.slice(end)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
export { mergePnpmBuildApprovals, requiredPnpmBuildApprovals };
|
|
46
|
+
//# sourceMappingURL=pnpm-build-approvals.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pnpm-build-approvals.mjs","names":[],"sources":["../../../../../../create-warlock/src/helpers/pnpm-build-approvals.ts"],"sourcesContent":["/** Native dependency build scripts required by features the scaffold explicitly selected. */\nconst FEATURE_BUILD_APPROVALS: Readonly<Record<string, readonly string[]>> = {\n image: [\"sharp\"],\n};\n\nexport function requiredPnpmBuildApprovals(\n features: readonly string[],\n): readonly string[] {\n return [\n ...new Set(\n features.flatMap(feature => FEATURE_BUILD_APPROVALS[feature] ?? []),\n ),\n ];\n}\n\n/**\n * Add only known feature-required builds to pnpm 11's workspace policy.\n * Existing `false` values are user decisions and are never overridden.\n */\nexport function mergePnpmBuildApprovals(\n yaml: string,\n packages: readonly string[],\n): string {\n let result = yaml;\n for (const packageName of packages) {\n result = mergeOneApproval(result, packageName);\n }\n return result;\n}\n\nconst quotedKey = (name: string) => `(?:${name}|\"${name}\"|'${name}')`;\n\n/** Edit one ordinary YAML mapping while preserving every untouched byte. */\nfunction mergeOneApproval(yaml: string, packageName: string): string {\n const escaped = packageName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const block = /^allowBuilds:\\s*(?:#.*)?(?:\\r?\\n|$)/m.exec(yaml);\n\n if (!block) {\n // Flow mappings are deliberately not rewritten. A hand parser must never\n // turn a decided flow entry into a duplicate block key.\n if (/^(?:allowBuilds|\"allowBuilds\"|'allowBuilds')\\s*:/m.test(yaml))\n return yaml;\n return `${yaml.replace(/\\s*$/, \"\")}\\nallowBuilds:\\n ${packageName}: true\\n`;\n }\n\n const start = block.index + block[0].length;\n const tail = yaml.slice(start);\n const nextTopLevel = /^(?!\\s|#|$)[^\\r\\n]+/m.exec(tail);\n const end = nextTopLevel ? start + nextTopLevel.index : yaml.length;\n const mapping = yaml.slice(start, end);\n const key = quotedKey(escaped);\n const entry = new RegExp(\n `^(\\\\s*)${key}\\\\s*:\\\\s*([^#\\\\r\\\\n]*)(.*)$`,\n \"m\",\n ).exec(mapping);\n\n if (entry) {\n const value = entry[2]!.trim();\n if (value === \"true\" || value === \"false\") return yaml;\n if (\n !/^(?:set this to true or false|\"set this to true or false\"|'set this to true or false')$/.test(\n value,\n )\n )\n return yaml;\n // pnpm's undecided placeholder belongs to this generated mapping: decide it\n // in place rather than emitting a duplicate YAML key.\n const comment = entry[3] ?? \"\";\n const replacement = `${entry[1]}${packageName}: true${comment.startsWith(\"#\") ? \" \" : \"\"}${comment}`;\n return `${yaml.slice(0, start)}${mapping.slice(0, entry.index)}${replacement}${mapping.slice(entry.index + entry[0].length)}${yaml.slice(end)}`;\n }\n\n const indentation = /^([ \\t]+)\\S/m.exec(mapping)?.[1] ?? \" \";\n return `${yaml.slice(0, start)}${indentation}${packageName}: true\\n${mapping}${yaml.slice(end)}`;\n}\n"],"mappings":";;AACA,MAAM,0BAAuE,EAC3E,OAAO,CAAC,OAAO,EACjB;AAEA,SAAgB,2BACd,UACmB;CACnB,OAAO,CACL,GAAG,IAAI,IACL,SAAS,SAAQ,YAAW,wBAAwB,YAAY,CAAC,CAAC,CACpE,CACF;AACF;;;;;AAMA,SAAgB,wBACd,MACA,UACQ;CACR,IAAI,SAAS;CACb,KAAK,MAAM,eAAe,UACxB,SAAS,iBAAiB,QAAQ,WAAW;CAE/C,OAAO;AACT;AAEA,MAAM,aAAa,SAAiB,MAAM,KAAK,IAAI,KAAK,KAAK,KAAK;;AAGlE,SAAS,iBAAiB,MAAc,aAA6B;CACnE,MAAM,UAAU,YAAY,QAAQ,uBAAuB,MAAM;CACjE,MAAM,QAAQ,uCAAuC,KAAK,IAAI;CAE9D,IAAI,CAAC,OAAO;EAGV,IAAI,oDAAoD,KAAK,IAAI,GAC/D,OAAO;EACT,OAAO,GAAG,KAAK,QAAQ,QAAQ,EAAE,EAAE,oBAAoB,YAAY;CACrE;CAEA,MAAM,QAAQ,MAAM,QAAQ,MAAM,EAAE,CAAC;CACrC,MAAM,OAAO,KAAK,MAAM,KAAK;CAC7B,MAAM,eAAe,uBAAuB,KAAK,IAAI;CACrD,MAAM,MAAM,eAAe,QAAQ,aAAa,QAAQ,KAAK;CAC7D,MAAM,UAAU,KAAK,MAAM,OAAO,GAAG;CACrC,MAAM,MAAM,UAAU,OAAO;CAC7B,MAAM,QAAQ,IAAI,OAChB,UAAU,IAAI,8BACd,GACF,CAAC,CAAC,KAAK,OAAO;CAEd,IAAI,OAAO;EACT,MAAM,QAAQ,MAAM,EAAE,CAAE,KAAK;EAC7B,IAAI,UAAU,UAAU,UAAU,SAAS,OAAO;EAClD,IACE,CAAC,0FAA0F,KACzF,KACF,GAEA,OAAO;EAGT,MAAM,UAAU,MAAM,MAAM;EAC5B,MAAM,cAAc,GAAG,MAAM,KAAK,YAAY,QAAQ,QAAQ,WAAW,GAAG,IAAI,MAAM,KAAK;EAC3F,OAAO,GAAG,KAAK,MAAM,GAAG,KAAK,IAAI,QAAQ,MAAM,GAAG,MAAM,KAAK,IAAI,cAAc,QAAQ,MAAM,MAAM,QAAQ,MAAM,EAAE,CAAC,MAAM,IAAI,KAAK,MAAM,GAAG;CAC9I;CAEA,MAAM,cAAc,eAAe,KAAK,OAAO,CAAC,GAAG,MAAM;CACzD,OAAO,GAAG,KAAK,MAAM,GAAG,KAAK,IAAI,cAAc,YAAY,UAAU,UAAU,KAAK,MAAM,GAAG;AAC/F"}
|
package/llms-full.txt
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
name: create-a-warlock-project
|
|
11
|
-
description:
|
|
11
|
+
description: "Create a Warlock project with the canonical scaffold, package selection, and project conventions."
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# create-warlock — scaffold a new project
|
|
@@ -83,6 +83,12 @@ Using npm · database: none · agent-kit targets: claude
|
|
|
83
83
|
| `--agents` | yes (CSV) | `claude` | Comma-separated `agent-kit` targets — see below. |
|
|
84
84
|
| `--git` / `--no-git` | no | off | Initialize a Git repo (`git init` + `main` branch + initial commit).|
|
|
85
85
|
| `--jwt` / `--no-jwt` | no | off | Generate JWT secrets via the project's `jwt` script. |
|
|
86
|
+
|
|
87
|
+
With `--pm=pnpm`, selecting the `image` feature records only Sharp's required
|
|
88
|
+
native build approval in the generated `pnpm-workspace.yaml` before install.
|
|
89
|
+
An explicit existing denial is retained. If selected feature dependencies do
|
|
90
|
+
not install, JWT generation is skipped and reported rather than retried through
|
|
91
|
+
the package manager.
|
|
86
92
|
| `--yes` / `-y` | no | off | Fully headless mode. |
|
|
87
93
|
| `--interactive`, `--customize` | no | off | Restore the full long-form wizard. |
|
|
88
94
|
|
package/llms.txt
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
|
|
7
7
|
## Skills
|
|
8
8
|
|
|
9
|
-
- [create-a-warlock-project](create-warlock/create-a-warlock-project/SKILL.md):
|
|
9
|
+
- [create-a-warlock-project](create-warlock/create-a-warlock-project/SKILL.md): Create a Warlock project with the canonical scaffold, package selection, and project conventions.
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@clack/prompts": "^0.7.0",
|
|
14
14
|
"@mongez/copper": "^2.1.2",
|
|
15
|
-
"@warlock.js/fs": "5.
|
|
15
|
+
"@warlock.js/fs": "5.19.0",
|
|
16
16
|
"@mongez/reinforcements": "^4.0.1",
|
|
17
17
|
"cross-spawn": "^7.0.3",
|
|
18
18
|
"rimraf": "^6.0.1",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=22.13"
|
|
23
23
|
},
|
|
24
|
-
"version": "5.
|
|
24
|
+
"version": "5.19.0",
|
|
25
25
|
"type": "module",
|
|
26
26
|
"main": "./esm/index.mjs",
|
|
27
27
|
"module": "./esm/index.mjs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: create-a-warlock-project
|
|
3
|
-
description:
|
|
3
|
+
description: "Create a Warlock project with the canonical scaffold, package selection, and project conventions."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# create-warlock — scaffold a new project
|
|
@@ -75,6 +75,12 @@ Using npm · database: none · agent-kit targets: claude
|
|
|
75
75
|
| `--agents` | yes (CSV) | `claude` | Comma-separated `agent-kit` targets — see below. |
|
|
76
76
|
| `--git` / `--no-git` | no | off | Initialize a Git repo (`git init` + `main` branch + initial commit).|
|
|
77
77
|
| `--jwt` / `--no-jwt` | no | off | Generate JWT secrets via the project's `jwt` script. |
|
|
78
|
+
|
|
79
|
+
With `--pm=pnpm`, selecting the `image` feature records only Sharp's required
|
|
80
|
+
native build approval in the generated `pnpm-workspace.yaml` before install.
|
|
81
|
+
An explicit existing denial is retained. If selected feature dependencies do
|
|
82
|
+
not install, JWT generation is skipped and reported rather than retried through
|
|
83
|
+
the package manager.
|
|
78
84
|
| `--yes` / `-y` | no | off | Fully headless mode. |
|
|
79
85
|
| `--interactive`, `--customize` | no | off | Restore the full long-form wizard. |
|
|
80
86
|
|
|
@@ -59,7 +59,7 @@ automatically on `postinstall`).
|
|
|
59
59
|
- **code-standards** — TypeScript style: `interface` (contracts) vs `type` (data),
|
|
60
60
|
access modifiers on every class member, no `any`, single-responsibility files
|
|
61
61
|
with the documented naming suffixes, JSDoc on the public surface.
|
|
62
|
-
- **web-code-standards** —
|
|
62
|
+
- **web-code-standards** — `src/web/**` feature boundaries, page/setup ownership, components, hooks, CSS, locales, and accessible browser rendering.
|
|
63
63
|
- **data-and-persistence** — modeling, storing, and migrating data with
|
|
64
64
|
`@warlock.js/cascade` (money as integer minor units, migrations, indexes).
|
|
65
65
|
- **module-boundaries** — how modules under `src/app/**` relate; one domain noun
|
|
@@ -38,6 +38,14 @@ If the business has an "orders team" and a "payments team", you probably have an
|
|
|
38
38
|
|
|
39
39
|
Every module follows this folder shape. Not every folder is required — only the ones the module actually uses.
|
|
40
40
|
|
|
41
|
+
`src/web/<feature>/` follows the same ownership rule as an app module: one
|
|
42
|
+
feature owns its presentation, browser behavior, feature utilities, styles,
|
|
43
|
+
and locales; backend domain decisions stay in `src/app/<domain>/services`.
|
|
44
|
+
Web features use `components/`, `hooks/`, and `utils/` without barrel indexes.
|
|
45
|
+
They use `types/` for several contracts or a single `<feature>.types.ts` for a
|
|
46
|
+
small one. This web exception does not permit a catch-all `lib/` folder or
|
|
47
|
+
cross-feature imports into private implementation files.
|
|
48
|
+
|
|
41
49
|
```
|
|
42
50
|
src/app/<module>/
|
|
43
51
|
controllers/ ← HTTP handlers (one per endpoint)
|
|
@@ -42,6 +42,15 @@ directory namespace. Every flattened translation key must have one owner;
|
|
|
42
42
|
ancestor and child dictionaries cannot redefine the same key. Keep legacy
|
|
43
43
|
`register()` dictionaries only where global translations are still needed.
|
|
44
44
|
|
|
45
|
+
Within a web feature, use `components/`, `hooks/`, and `utils/` for their
|
|
46
|
+
named purpose; do not create `lib/` as a catch-all. These folders have no
|
|
47
|
+
barrel `index.ts`: import the named component, hook, or helper from its own
|
|
48
|
+
file. Keep types in `types/` when a feature has several contracts, or in one
|
|
49
|
+
`<feature>.types.ts` file when they are small. Do not duplicate a contract in
|
|
50
|
+
both places. Put component props in that dedicated type module by default;
|
|
51
|
+
keep a tiny private prop type beside a component only when extracting it would
|
|
52
|
+
make the feature harder to read.
|
|
53
|
+
|
|
45
54
|
## 1. Keep the page contract, make the implementation thin
|
|
46
55
|
|
|
47
56
|
Warlock's file-routed modules have one policy export: `config`. A page's
|
|
@@ -135,6 +144,35 @@ output; the page composes `PostArticle`, `PostComments`, and `RelatedPosts`;
|
|
|
135
144
|
`use-post-comments.ts` owns subscription, state, and handlers for
|
|
136
145
|
`PostComments`. Each piece has one reason to change.
|
|
137
146
|
|
|
147
|
+
## Setup and component files
|
|
148
|
+
|
|
149
|
+
Use a setup file when a feature registers framework behavior. The setup file
|
|
150
|
+
owns its `register`, `config`, and `loader` exports; its matching component
|
|
151
|
+
only renders the supplied data. Keep loader inference type-only so it does
|
|
152
|
+
not pull server work into a browser component:
|
|
153
|
+
|
|
154
|
+
The default starter follows this boundary: `root.setup.ts` owns strict-mode
|
|
155
|
+
configuration, `home/index.setup.ts` owns route policy, its loader, and the
|
|
156
|
+
universal translations `register()`, and `404.setup.ts` owns not-found
|
|
157
|
+
metadata. Do not add an empty setup file when a component has no server
|
|
158
|
+
configuration, loader, or registration work.
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
import type { PageProps } from "@warlock.js/web";
|
|
162
|
+
import type { loader } from "./posts.setup";
|
|
163
|
+
|
|
164
|
+
export default function PostsPage({ data }: PageProps<typeof loader>) {
|
|
165
|
+
return <PostsList posts={data.posts} />;
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Each React component file has one named default component. Export hooks,
|
|
170
|
+
utilities, and types by name. A component renders; its feature hook owns
|
|
171
|
+
state, effects, handlers, navigation, and client mutations. A utility stays
|
|
172
|
+
pure and has no React or browser lifecycle dependency. Keep feature CSS beside
|
|
173
|
+
the feature and import it from the owning presentation boundary; keep loading,
|
|
174
|
+
error, empty, focus, reduced-motion, and locale behavior with that boundary.
|
|
175
|
+
|
|
138
176
|
## 2. Make the server-to-browser data contract explicit
|
|
139
177
|
|
|
140
178
|
- Validate URL parameters and query input at the page boundary. Use the
|
|
@@ -1,40 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { getHomeService } from "app/home/services/home.service";
|
|
3
|
-
import { isLocaleCode } from "../../shared/locales";
|
|
1
|
+
import type { PageProps } from "@warlock.js/web";
|
|
4
2
|
import { ContactSection } from "./components/contact-section";
|
|
5
3
|
import { ContentSections } from "./components/content-sections";
|
|
6
4
|
import { HeroSection } from "./components/hero-section";
|
|
7
5
|
import { HomeFooter } from "./components/home-footer";
|
|
8
6
|
import { HomeHeader } from "./components/home-header";
|
|
7
|
+
import type { loader } from "./index.setup";
|
|
9
8
|
import "./styles/home.css";
|
|
10
9
|
|
|
11
|
-
export {
|
|
12
|
-
|
|
13
|
-
type HomeLoaderOptions = Parameters<PageLoader>[0];
|
|
14
|
-
|
|
15
|
-
export async function loader({ request, response }: HomeLoaderOptions) {
|
|
16
|
-
const locale = request.locale;
|
|
17
|
-
if (!isLocaleCode(locale)) {
|
|
18
|
-
return response.notFound();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const homeData = await getHomeService();
|
|
22
|
-
|
|
23
|
-
return { locale, ...homeData };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const config = {
|
|
27
|
-
route: { path: "/", name: "home" },
|
|
28
|
-
metadata: {
|
|
29
|
-
title: "Warlock.js — Build with uncommon power",
|
|
30
|
-
description:
|
|
31
|
-
"A TypeScript framework for production backends, server-rendered React applications, and AI-native systems.",
|
|
32
|
-
},
|
|
33
|
-
} satisfies PageConfig<typeof loader>;
|
|
34
|
-
|
|
35
|
-
type HomePageProps = PageProps<typeof loader>;
|
|
36
|
-
|
|
37
|
-
export default function HomePage({ data }: HomePageProps) {
|
|
10
|
+
export default function HomePage({ data }: PageProps<typeof loader>) {
|
|
38
11
|
return (
|
|
39
12
|
<main className="warlock-home">
|
|
40
13
|
<HomeHeader />
|
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
import { groupedTranslations } from "@mongez/localization";
|
|
2
|
+
import type { PageConfig, PageLoader } from "@warlock.js/web";
|
|
3
|
+
import { getHomeService } from "app/home/services/home.service";
|
|
4
|
+
import { isLocaleCode } from "../../shared/locales";
|
|
5
|
+
|
|
6
|
+
type HomeLoaderOptions = Parameters<PageLoader>[0];
|
|
7
|
+
|
|
8
|
+
export async function loader({ request, response }: HomeLoaderOptions) {
|
|
9
|
+
const locale = request.locale;
|
|
10
|
+
|
|
11
|
+
if (!isLocaleCode(locale)) {
|
|
12
|
+
return response.notFound();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const homeData = await getHomeService();
|
|
16
|
+
|
|
17
|
+
return { locale, ...homeData };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const config = {
|
|
21
|
+
route: { path: "/", name: "home" },
|
|
22
|
+
metadata: {
|
|
23
|
+
title: "Warlock.js — Build with uncommon power",
|
|
24
|
+
description:
|
|
25
|
+
"A TypeScript framework for production backends, server-rendered React applications, and AI-native systems.",
|
|
26
|
+
},
|
|
27
|
+
} satisfies PageConfig<typeof loader>;
|
|
2
28
|
|
|
3
29
|
export function register() {
|
|
4
30
|
groupedTranslations({
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import type { AppProps
|
|
1
|
+
import type { AppProps } from "@warlock.js/web";
|
|
2
2
|
import { Head, Scripts, useLocale, useTextDirection } from "@warlock.js/web";
|
|
3
3
|
import "./app.css";
|
|
4
4
|
|
|
5
|
-
/** Enable React's development checks for the hydrated page/layout tree. */
|
|
6
|
-
export const config = { strictMode: true } satisfies RootConfig;
|
|
7
|
-
|
|
8
5
|
/**
|
|
9
6
|
* The application root.
|
|
10
7
|
*
|