@lenne.tech/cli 1.9.6 → 1.11.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/README.md +88 -3
- package/build/commands/config/validate.js +2 -0
- package/build/commands/frontend/convert-mode.js +198 -0
- package/build/commands/fullstack/convert-mode.js +368 -0
- package/build/commands/fullstack/init.js +150 -4
- package/build/commands/fullstack/update.js +177 -0
- package/build/commands/server/add-property.js +29 -2
- package/build/commands/server/convert-mode.js +197 -0
- package/build/commands/server/create.js +41 -3
- package/build/commands/server/module.js +58 -25
- package/build/commands/server/object.js +26 -5
- package/build/commands/server/permissions.js +20 -6
- package/build/commands/server/test.js +7 -1
- package/build/commands/status.js +94 -3
- package/build/config/vendor-frontend-runtime-deps.json +4 -0
- package/build/config/vendor-runtime-deps.json +9 -0
- package/build/extensions/api-mode.js +19 -3
- package/build/extensions/frontend-helper.js +652 -0
- package/build/extensions/server.js +1475 -3
- package/build/lib/framework-detection.js +167 -0
- package/build/lib/frontend-framework-detection.js +129 -0
- package/build/templates/nest-server-module/inputs/template-create.input.ts.ejs +1 -1
- package/build/templates/nest-server-module/inputs/template.input.ts.ejs +1 -1
- package/build/templates/nest-server-module/outputs/template-fac-result.output.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.controller.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.model.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.module.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.resolver.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.service.ts.ejs +1 -1
- package/build/templates/nest-server-object/template-create.input.ts.ejs +1 -1
- package/build/templates/nest-server-object/template.input.ts.ejs +1 -1
- package/build/templates/nest-server-object/template.object.ts.ejs +1 -1
- package/build/templates/nest-server-tests/tests.e2e-spec.ts.ejs +1 -1
- package/docs/LT-ECOSYSTEM-GUIDE.md +973 -0
- package/docs/VENDOR-MODE-WORKFLOW.md +471 -0
- package/docs/commands.md +196 -0
- package/docs/lt.config.md +9 -7
- package/package.json +17 -8
|
@@ -255,7 +255,25 @@ class Server {
|
|
|
255
255
|
}
|
|
256
256
|
useDefineForClassFieldsActivated() {
|
|
257
257
|
var _a;
|
|
258
|
-
|
|
258
|
+
// Walk UP from the current working directory to find the nearest
|
|
259
|
+
// tsconfig.json. gluegun's `filesystem.resolve('tsconfig.json')` resolves
|
|
260
|
+
// relative to cwd without checking existence, so it breaks when `lt
|
|
261
|
+
// server module` is invoked from inside `src/` (where no tsconfig lives)
|
|
262
|
+
// — it would then silently return `false`, causing the generator to
|
|
263
|
+
// emit class fields without the `override` modifier that the project's
|
|
264
|
+
// `noImplicitOverride` rule requires.
|
|
265
|
+
const path = require('path');
|
|
266
|
+
let current = this.filesystem.cwd();
|
|
267
|
+
const root = path.parse(current).root;
|
|
268
|
+
let tsConfigPath = null;
|
|
269
|
+
while (current && current !== root) {
|
|
270
|
+
const candidate = path.join(current, 'tsconfig.json');
|
|
271
|
+
if (this.filesystem.exists(candidate)) {
|
|
272
|
+
tsConfigPath = candidate;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
current = path.dirname(current);
|
|
276
|
+
}
|
|
259
277
|
if (tsConfigPath) {
|
|
260
278
|
const readConfig = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
|
|
261
279
|
if (!readConfig.error) {
|
|
@@ -614,7 +632,7 @@ class Server {
|
|
|
614
632
|
setupServer(dest, options) {
|
|
615
633
|
return __awaiter(this, void 0, void 0, function* () {
|
|
616
634
|
const { apiMode: apiModeHelper, patching, system, template, templateHelper } = this.toolbox;
|
|
617
|
-
const { apiMode, author = '', branch, copyPath, description = '', linkPath, name, projectDir, skipInstall = false, skipPatching = false, } = options;
|
|
635
|
+
const { apiMode, author = '', branch, copyPath, description = '', frameworkMode = 'npm', frameworkUpstreamBranch, linkPath, name, projectDir, skipInstall = false, skipPatching = false, } = options;
|
|
618
636
|
// Setup template
|
|
619
637
|
const result = yield templateHelper.setup(dest, {
|
|
620
638
|
branch,
|
|
@@ -677,6 +695,27 @@ class Server {
|
|
|
677
695
|
this.filesystem.remove(`${dest}/.yalc`);
|
|
678
696
|
this.filesystem.remove(`${dest}/yalc.lock`);
|
|
679
697
|
}
|
|
698
|
+
// Vendor-mode transformation — identical to setupServerForFullstack, so
|
|
699
|
+
// a standalone `lt server create --framework-mode vendor` produces the
|
|
700
|
+
// same project layout as `lt fullstack init --framework-mode vendor`.
|
|
701
|
+
// Essentials list is captured BEFORE processApiMode deletes the
|
|
702
|
+
// manifest (same dance as in setupServerForFullstack).
|
|
703
|
+
let standaloneVendorUpstreamDeps = {};
|
|
704
|
+
let standaloneVendorCoreEssentials = [];
|
|
705
|
+
if (frameworkMode === 'vendor') {
|
|
706
|
+
try {
|
|
707
|
+
const converted = yield this.convertCloneToVendored({
|
|
708
|
+
dest,
|
|
709
|
+
projectName: name,
|
|
710
|
+
upstreamBranch: frameworkUpstreamBranch,
|
|
711
|
+
});
|
|
712
|
+
standaloneVendorUpstreamDeps = converted.upstreamDeps;
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
return { method: result.method, path: dest, success: false };
|
|
716
|
+
}
|
|
717
|
+
standaloneVendorCoreEssentials = this.readApiModeGraphqlEssentials(dest);
|
|
718
|
+
}
|
|
680
719
|
// Process API mode (before install so package.json is correct)
|
|
681
720
|
if (apiMode) {
|
|
682
721
|
try {
|
|
@@ -686,6 +725,19 @@ class Server {
|
|
|
686
725
|
return { method: result.method, path: dest, success: false };
|
|
687
726
|
}
|
|
688
727
|
}
|
|
728
|
+
// Restore core essentials after processApiMode stripped them (vendor + REST only).
|
|
729
|
+
if (frameworkMode === 'vendor' && apiMode === 'Rest') {
|
|
730
|
+
try {
|
|
731
|
+
this.restoreVendorCoreEssentials({
|
|
732
|
+
dest,
|
|
733
|
+
essentials: standaloneVendorCoreEssentials,
|
|
734
|
+
upstreamDeps: standaloneVendorUpstreamDeps,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
catch (_a) {
|
|
738
|
+
// Non-fatal.
|
|
739
|
+
}
|
|
740
|
+
}
|
|
689
741
|
// Patch CLAUDE.md with API mode info
|
|
690
742
|
this.patchClaudeMdApiMode(dest, apiMode);
|
|
691
743
|
// Install packages
|
|
@@ -711,7 +763,21 @@ class Server {
|
|
|
711
763
|
setupServerForFullstack(dest, options) {
|
|
712
764
|
return __awaiter(this, void 0, void 0, function* () {
|
|
713
765
|
const { apiMode: apiModeHelper, templateHelper } = this.toolbox;
|
|
714
|
-
const { apiMode, branch, copyPath, linkPath, name, projectDir } = options;
|
|
766
|
+
const { apiMode, branch, copyPath, frameworkMode = 'npm', frameworkUpstreamBranch, linkPath, name, projectDir, } = options;
|
|
767
|
+
// Both npm and vendor mode clone nest-server-starter as the base. The
|
|
768
|
+
// starter ships the minimal consumer conventions a project needs
|
|
769
|
+
// (src/server/common/models/persistence.model.ts, src/server/modules/user/,
|
|
770
|
+
// file/, meta/, tests/, migrations/, env files, etc.).
|
|
771
|
+
//
|
|
772
|
+
// In vendor mode we additionally clone @lenne.tech/nest-server to
|
|
773
|
+
// obtain the framework `core/` tree, copy it into the project at
|
|
774
|
+
// src/core/ (with the flatten-fix), remove the `@lenne.tech/nest-server`
|
|
775
|
+
// npm dependency, merge its transitive deps into the project
|
|
776
|
+
// package.json, and run a codemod that rewrites every
|
|
777
|
+
// `from '@lenne.tech/nest-server'` import to a relative path pointing
|
|
778
|
+
// at the vendored core.
|
|
779
|
+
//
|
|
780
|
+
// See convertCloneToVendored() below for the exact transformation.
|
|
715
781
|
// Setup template
|
|
716
782
|
const result = yield templateHelper.setup(dest, {
|
|
717
783
|
branch,
|
|
@@ -749,6 +815,38 @@ class Server {
|
|
|
749
815
|
this.filesystem.remove(`${dest}/.yalc`);
|
|
750
816
|
this.filesystem.remove(`${dest}/yalc.lock`);
|
|
751
817
|
}
|
|
818
|
+
// Vendor-mode transformation: strip framework-internal content and wire
|
|
819
|
+
// the remaining files so they behave like a project that consumed the
|
|
820
|
+
// framework's core/ directory directly. Idempotent; safe to skip in
|
|
821
|
+
// npm mode.
|
|
822
|
+
//
|
|
823
|
+
// We capture the framework package.json snapshot from the temp clone so
|
|
824
|
+
// the post-apiMode step can restore upstream-declared core essentials
|
|
825
|
+
// without hard-coding package lists.
|
|
826
|
+
let vendorUpstreamDeps = {};
|
|
827
|
+
let vendorCoreEssentials = [];
|
|
828
|
+
if (frameworkMode === 'vendor') {
|
|
829
|
+
try {
|
|
830
|
+
const converted = yield this.convertCloneToVendored({
|
|
831
|
+
dest,
|
|
832
|
+
projectName: name,
|
|
833
|
+
upstreamBranch: frameworkUpstreamBranch,
|
|
834
|
+
});
|
|
835
|
+
vendorUpstreamDeps = converted.upstreamDeps;
|
|
836
|
+
}
|
|
837
|
+
catch (err) {
|
|
838
|
+
return { method: result.method, path: dest, success: false };
|
|
839
|
+
}
|
|
840
|
+
// Read the graphql-only package list from the starter's
|
|
841
|
+
// api-mode.manifest.json BEFORE processApiMode runs and deletes it.
|
|
842
|
+
// These are exactly the packages processApiMode will strip in REST
|
|
843
|
+
// mode — and in vendor mode they must come back afterwards, because
|
|
844
|
+
// src/core/** still imports them even when the consumer project is
|
|
845
|
+
// REST-only (e.g. PubSub in core-auth.module.ts, GraphQLUpload in
|
|
846
|
+
// core-file.service.ts). List is dynamic; new additions surface
|
|
847
|
+
// automatically.
|
|
848
|
+
vendorCoreEssentials = this.readApiModeGraphqlEssentials(dest);
|
|
849
|
+
}
|
|
752
850
|
// Process API mode (before install which happens at monorepo level)
|
|
753
851
|
if (apiMode) {
|
|
754
852
|
try {
|
|
@@ -758,11 +856,1029 @@ class Server {
|
|
|
758
856
|
return { method: result.method, path: dest, success: false };
|
|
759
857
|
}
|
|
760
858
|
}
|
|
859
|
+
// In vendor mode + REST, re-add the graphql essentials that
|
|
860
|
+
// processApiMode just stripped. Both and GraphQL keep all packages
|
|
861
|
+
// by construction and don't need restoration.
|
|
862
|
+
if (frameworkMode === 'vendor' && apiMode === 'Rest') {
|
|
863
|
+
try {
|
|
864
|
+
this.restoreVendorCoreEssentials({
|
|
865
|
+
dest,
|
|
866
|
+
essentials: vendorCoreEssentials,
|
|
867
|
+
upstreamDeps: vendorUpstreamDeps,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
catch (err) {
|
|
871
|
+
// Non-fatal — install may still succeed if the core never imports
|
|
872
|
+
// the restored packages at the current version.
|
|
873
|
+
}
|
|
874
|
+
}
|
|
761
875
|
// Patch CLAUDE.md with API mode info
|
|
762
876
|
this.patchClaudeMdApiMode(dest, apiMode);
|
|
763
877
|
return { method: result.method, path: dest, success: true };
|
|
764
878
|
});
|
|
765
879
|
}
|
|
880
|
+
/**
|
|
881
|
+
* Converts a freshly cloned `nest-server-starter` working tree into a
|
|
882
|
+
* vendored-mode consumer project.
|
|
883
|
+
*
|
|
884
|
+
* The starter ships all consumer conventions a project needs (a
|
|
885
|
+
* working `src/server/` with `common/models/persistence.model.ts`,
|
|
886
|
+
* `modules/user/`, `modules/file/`, `modules/meta/`, sample tests,
|
|
887
|
+
* migrations, env files). In npm mode it relies on the
|
|
888
|
+
* `@lenne.tech/nest-server` npm dependency to provide the framework
|
|
889
|
+
* source via `node_modules/@lenne.tech/nest-server/dist/**`.
|
|
890
|
+
*
|
|
891
|
+
* In vendor mode we additionally clone `@lenne.tech/nest-server` to
|
|
892
|
+
* /tmp, copy its framework kernel (`src/core/`, `src/index.ts`,
|
|
893
|
+
* `src/core.module.ts`, `src/test/`, `src/templates/`, `src/types/`,
|
|
894
|
+
* `LICENSE`, `bin/migrate.js`) into the project at `src/core/`
|
|
895
|
+
* applying the flatten-fix, remove `@lenne.tech/nest-server` from the
|
|
896
|
+
* project's `package.json`, merge the framework's transitive deps into
|
|
897
|
+
* the project's own deps, and run an AST-based codemod that rewrites
|
|
898
|
+
* every `from '@lenne.tech/nest-server'` import in consumer code
|
|
899
|
+
* (src/server, src/main.ts, tests/, migrations/, scripts/) to a
|
|
900
|
+
* relative path pointing at the vendored `src/core/`.
|
|
901
|
+
*
|
|
902
|
+
* The resulting tree matches the layout produced by the imo vendoring
|
|
903
|
+
* pilot, so the `nest-server-core-updater` and
|
|
904
|
+
* `nest-server-core-contributor` agents work without any further
|
|
905
|
+
* post-processing.
|
|
906
|
+
*
|
|
907
|
+
* Idempotent — running twice is a no-op.
|
|
908
|
+
*/
|
|
909
|
+
convertCloneToVendored(options) {
|
|
910
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
911
|
+
const { dest, upstreamBranch, upstreamRepoUrl = 'https://github.com/lenneTech/nest-server.git' } = options;
|
|
912
|
+
const filesystem = this.filesystem;
|
|
913
|
+
const { system } = this.toolbox;
|
|
914
|
+
const os = require('os');
|
|
915
|
+
const path = require('path');
|
|
916
|
+
const { Project, SyntaxKind } = require('ts-morph');
|
|
917
|
+
const srcDir = `${dest}/src`;
|
|
918
|
+
const coreDir = `${srcDir}/core`;
|
|
919
|
+
// ── 1. Clone @lenne.tech/nest-server into a temp directory ───────────
|
|
920
|
+
//
|
|
921
|
+
// We clone the framework repo shallowly to get the `src/core/` tree,
|
|
922
|
+
// `bin/migrate.js`, and associated meta files. The clone lives in a
|
|
923
|
+
// throw-away tmp dir that gets cleaned up at the end.
|
|
924
|
+
const tmpClone = path.join(os.tmpdir(), `lt-vendor-nest-server-${Date.now()}`);
|
|
925
|
+
const branchArg = upstreamBranch ? `--branch ${upstreamBranch} ` : '';
|
|
926
|
+
try {
|
|
927
|
+
yield system.run(`git clone --depth 1 ${branchArg}${upstreamRepoUrl} ${tmpClone}`);
|
|
928
|
+
}
|
|
929
|
+
catch (err) {
|
|
930
|
+
// Clone failures usually boil down to one of four causes — network,
|
|
931
|
+
// auth, unknown ref, or a pre-existing tmp dir. Give the user a
|
|
932
|
+
// pointed error message rather than the raw `git clone` stderr.
|
|
933
|
+
const raw = err.message || '';
|
|
934
|
+
const hints = [];
|
|
935
|
+
if (/Could not resolve host|getaddrinfo|ECONNREFUSED|Network is unreachable/i.test(raw)) {
|
|
936
|
+
hints.push('Network issue reaching github.com — check your connection or proxy settings.');
|
|
937
|
+
}
|
|
938
|
+
if (/Permission denied|authentication failed|publickey|403|401/i.test(raw)) {
|
|
939
|
+
hints.push('Authentication issue — the CLI uses an anonymous HTTPS clone; verify GitHub is reachable.');
|
|
940
|
+
}
|
|
941
|
+
if (upstreamBranch && /Remote branch .* not found|did not match any file\(s\) known to git/i.test(raw)) {
|
|
942
|
+
hints.push(`Upstream ref "${upstreamBranch}" does not exist. Check ${upstreamRepoUrl}/tags or /branches for valid refs. ` +
|
|
943
|
+
'Note: nest-server tags have NO "v" prefix — use e.g. "11.24.1", not "v11.24.1".');
|
|
944
|
+
}
|
|
945
|
+
if (/already exists and is not an empty/i.test(raw)) {
|
|
946
|
+
hints.push(`Target directory ${tmpClone} already exists. This usually indicates a stale previous run — rm -rf /tmp/lt-vendor-nest-server-* and retry.`);
|
|
947
|
+
}
|
|
948
|
+
const hintBlock = hints.length > 0 ? `\n Hints:\n - ${hints.join('\n - ')}` : '';
|
|
949
|
+
throw new Error(`Failed to clone ${upstreamRepoUrl}${upstreamBranch ? ` (branch/tag: ${upstreamBranch})` : ''}.\n Raw git error: ${raw.trim()}${hintBlock}`);
|
|
950
|
+
}
|
|
951
|
+
// Snapshot upstream package.json before cleanup so we can merge its
|
|
952
|
+
// transitive deps into the project's package.json (step 5 below).
|
|
953
|
+
let upstreamDeps = {};
|
|
954
|
+
let upstreamDevDeps = {};
|
|
955
|
+
let upstreamVersion = '';
|
|
956
|
+
try {
|
|
957
|
+
const upstreamPkg = filesystem.read(`${tmpClone}/package.json`, 'json');
|
|
958
|
+
if (upstreamPkg && typeof upstreamPkg === 'object') {
|
|
959
|
+
upstreamDeps = upstreamPkg.dependencies || {};
|
|
960
|
+
upstreamDevDeps = upstreamPkg.devDependencies || {};
|
|
961
|
+
upstreamVersion = upstreamPkg.version || '';
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
catch (_a) {
|
|
965
|
+
// Best-effort — if we can't read upstream pkg, the starter's own
|
|
966
|
+
// deps should still cover most of the framework's needs.
|
|
967
|
+
}
|
|
968
|
+
// Snapshot the upstream CLAUDE.md for section-merge into projects/api/CLAUDE.md.
|
|
969
|
+
// The nest-server CLAUDE.md contains framework-specific instructions that
|
|
970
|
+
// Claude Code needs to work correctly with the vendored source (API conventions,
|
|
971
|
+
// UnifiedField usage, CrudService patterns, etc.). We capture it before the
|
|
972
|
+
// temp clone is deleted and merge it after the vendor-marker block.
|
|
973
|
+
let upstreamClaudeMd = '';
|
|
974
|
+
try {
|
|
975
|
+
const claudeMdContent = filesystem.read(`${tmpClone}/CLAUDE.md`);
|
|
976
|
+
if (typeof claudeMdContent === 'string') {
|
|
977
|
+
upstreamClaudeMd = claudeMdContent;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
catch (_b) {
|
|
981
|
+
// Non-fatal — if missing, the project CLAUDE.md just won't get upstream sections.
|
|
982
|
+
}
|
|
983
|
+
// Snapshot the upstream commit SHA for traceability in VENDOR.md.
|
|
984
|
+
let upstreamCommit = '';
|
|
985
|
+
try {
|
|
986
|
+
const sha = yield system.run(`git -C ${tmpClone} rev-parse HEAD`);
|
|
987
|
+
upstreamCommit = (sha || '').trim();
|
|
988
|
+
}
|
|
989
|
+
catch (_c) {
|
|
990
|
+
// Non-fatal — VENDOR.md will just show an empty SHA.
|
|
991
|
+
}
|
|
992
|
+
try {
|
|
993
|
+
// ── 2. Copy framework kernel into project src/core/ (flatten-fix) ──
|
|
994
|
+
//
|
|
995
|
+
// Upstream layout: src/core/ (framework sub-dir) + src/index.ts +
|
|
996
|
+
// src/core.module.ts + src/test/ + src/templates/ + src/types/.
|
|
997
|
+
// Target layout: everything flat under <project>/src/core/.
|
|
998
|
+
//
|
|
999
|
+
// We WIPE the starter's (non-existent in npm mode) src/core/ first
|
|
1000
|
+
// just to guarantee idempotency when users run this twice.
|
|
1001
|
+
if (filesystem.exists(coreDir)) {
|
|
1002
|
+
filesystem.remove(coreDir);
|
|
1003
|
+
}
|
|
1004
|
+
const copies = [
|
|
1005
|
+
[`${tmpClone}/src/core`, coreDir],
|
|
1006
|
+
[`${tmpClone}/src/index.ts`, `${coreDir}/index.ts`],
|
|
1007
|
+
[`${tmpClone}/src/core.module.ts`, `${coreDir}/core.module.ts`],
|
|
1008
|
+
[`${tmpClone}/src/test`, `${coreDir}/test`],
|
|
1009
|
+
[`${tmpClone}/src/templates`, `${coreDir}/templates`],
|
|
1010
|
+
[`${tmpClone}/src/types`, `${coreDir}/types`],
|
|
1011
|
+
[`${tmpClone}/LICENSE`, `${coreDir}/LICENSE`],
|
|
1012
|
+
];
|
|
1013
|
+
for (const [from, to] of copies) {
|
|
1014
|
+
if (filesystem.exists(from)) {
|
|
1015
|
+
filesystem.copy(from, to);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
// Copy bin/migrate.js so the project has a working migrate CLI
|
|
1019
|
+
// independent of node_modules/@lenne.tech/nest-server.
|
|
1020
|
+
// Overwrite existing file if present (convert-mode on existing project).
|
|
1021
|
+
if (filesystem.exists(`${tmpClone}/bin/migrate.js`)) {
|
|
1022
|
+
if (filesystem.exists(`${dest}/bin/migrate.js`)) {
|
|
1023
|
+
filesystem.remove(`${dest}/bin/migrate.js`);
|
|
1024
|
+
}
|
|
1025
|
+
filesystem.copy(`${tmpClone}/bin/migrate.js`, `${dest}/bin/migrate.js`);
|
|
1026
|
+
}
|
|
1027
|
+
// Copy migration-guides for vendor-sync agent reference (optional
|
|
1028
|
+
// but useful — small overhead, big value for the updater agent).
|
|
1029
|
+
// Preserve any project-specific guides by merging instead of overwriting.
|
|
1030
|
+
if (filesystem.exists(`${tmpClone}/migration-guides`)) {
|
|
1031
|
+
if (filesystem.exists(`${dest}/migration-guides`)) {
|
|
1032
|
+
// Project already has migration-guides (maybe custom ones) — merge
|
|
1033
|
+
// upstream files into the existing directory without removing locals.
|
|
1034
|
+
const upstreamGuides = filesystem.find(`${tmpClone}/migration-guides`, {
|
|
1035
|
+
matching: '*.md',
|
|
1036
|
+
recursive: false,
|
|
1037
|
+
}) || [];
|
|
1038
|
+
for (const guide of upstreamGuides) {
|
|
1039
|
+
const basename = require('node:path').basename(guide);
|
|
1040
|
+
const source = `${tmpClone}/migration-guides/${basename}`;
|
|
1041
|
+
const target = `${dest}/migration-guides/${basename}`;
|
|
1042
|
+
if (filesystem.exists(target)) {
|
|
1043
|
+
filesystem.remove(target);
|
|
1044
|
+
}
|
|
1045
|
+
if (filesystem.exists(source)) {
|
|
1046
|
+
filesystem.copy(source, target);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
filesystem.copy(`${tmpClone}/migration-guides`, `${dest}/migration-guides`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
finally {
|
|
1056
|
+
// Always clean up the temp clone, even if copy fails midway.
|
|
1057
|
+
if (filesystem.exists(tmpClone)) {
|
|
1058
|
+
filesystem.remove(tmpClone);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
// ── 3. Apply flatten-fix rewrites on the vendored files ──────────────
|
|
1062
|
+
//
|
|
1063
|
+
// In `src/core/index.ts` and `src/core/core.module.ts` every relative
|
|
1064
|
+
// specifier that used to be `./core/…` (when the file lived on src/)
|
|
1065
|
+
// must now drop the `./core/` prefix. All OTHER internal imports
|
|
1066
|
+
// within common/, modules/, etc. stay identical because their
|
|
1067
|
+
// relative structure is preserved by the copy.
|
|
1068
|
+
//
|
|
1069
|
+
// Known edge cases from the imo pilot:
|
|
1070
|
+
// - src/core/test/test.helper.ts references '../core/common/helpers/db.helper'
|
|
1071
|
+
// which must become '../common/helpers/db.helper' after the flatten.
|
|
1072
|
+
// - src/core/common/interfaces/core-persistence-model.interface.ts
|
|
1073
|
+
// references '../../..' (three levels up to src/index.ts), which
|
|
1074
|
+
// must become '../..' (two levels up to src/core/index.ts).
|
|
1075
|
+
const tsMorphProject = new Project({ skipAddingFilesFromTsConfig: true });
|
|
1076
|
+
const flattenTargets = [`${coreDir}/index.ts`, `${coreDir}/core.module.ts`];
|
|
1077
|
+
for (const target of flattenTargets) {
|
|
1078
|
+
if (!filesystem.exists(target))
|
|
1079
|
+
continue;
|
|
1080
|
+
const sourceFile = tsMorphProject.addSourceFileAtPath(target);
|
|
1081
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
1082
|
+
const spec = decl.getModuleSpecifierValue();
|
|
1083
|
+
if (spec && spec.startsWith('./core/')) {
|
|
1084
|
+
decl.setModuleSpecifier(spec.replace(/^\.\/core\//, './'));
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
for (const decl of sourceFile.getExportDeclarations()) {
|
|
1088
|
+
const spec = decl.getModuleSpecifierValue();
|
|
1089
|
+
if (spec && spec.startsWith('./core/')) {
|
|
1090
|
+
decl.setModuleSpecifier(spec.replace(/^\.\/core\//, './'));
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
sourceFile.saveSync();
|
|
1094
|
+
}
|
|
1095
|
+
const testHelperPath = `${coreDir}/test/test.helper.ts`;
|
|
1096
|
+
if (filesystem.exists(testHelperPath)) {
|
|
1097
|
+
const sourceFile = tsMorphProject.addSourceFileAtPath(testHelperPath);
|
|
1098
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
1099
|
+
const spec = decl.getModuleSpecifierValue();
|
|
1100
|
+
if (spec && spec.startsWith('../core/')) {
|
|
1101
|
+
decl.setModuleSpecifier(spec.replace(/^\.\.\/core\//, '../'));
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
sourceFile.saveSync();
|
|
1105
|
+
}
|
|
1106
|
+
const persistenceIfacePath = `${coreDir}/common/interfaces/core-persistence-model.interface.ts`;
|
|
1107
|
+
if (filesystem.exists(persistenceIfacePath)) {
|
|
1108
|
+
const sourceFile = tsMorphProject.addSourceFileAtPath(persistenceIfacePath);
|
|
1109
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
1110
|
+
const spec = decl.getModuleSpecifierValue();
|
|
1111
|
+
if (spec === '../../..') {
|
|
1112
|
+
decl.setModuleSpecifier('../..');
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
sourceFile.saveSync();
|
|
1116
|
+
}
|
|
1117
|
+
// Edge: core-better-auth-user.mapper.ts uses `ScryptOptions` as a type
|
|
1118
|
+
// annotation without an explicit import. Upstream relies on older
|
|
1119
|
+
// @types/node versions where ScryptOptions was a global. With newer
|
|
1120
|
+
// @types/node (25+) it must be imported from 'crypto'. Add the import.
|
|
1121
|
+
const betterAuthMapperPath = `${coreDir}/modules/better-auth/core-better-auth-user.mapper.ts`;
|
|
1122
|
+
if (filesystem.exists(betterAuthMapperPath)) {
|
|
1123
|
+
const raw = filesystem.read(betterAuthMapperPath) || '';
|
|
1124
|
+
if (raw.includes('ScryptOptions') && !raw.includes('type ScryptOptions')) {
|
|
1125
|
+
// Replace the bare `randomBytes, scrypt` crypto import with one
|
|
1126
|
+
// that also pulls in the ScryptOptions type.
|
|
1127
|
+
const patched = raw.replace(/import\s+\{\s*randomBytes\s*,\s*scrypt\s*\}\s+from\s+['"]crypto['"]\s*;/, "import { randomBytes, scrypt, type ScryptOptions } from 'crypto';");
|
|
1128
|
+
if (patched !== raw) {
|
|
1129
|
+
filesystem.write(betterAuthMapperPath, patched);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
// Edge: express exports Request/Response as TYPE-ONLY. In npm-mode this
|
|
1134
|
+
// was not a problem because the framework shipped as pre-compiled dist/
|
|
1135
|
+
// and type imports were erased before runtime. In vendor-mode, vitest/vite
|
|
1136
|
+
// evaluates the TypeScript source directly and chokes with:
|
|
1137
|
+
// "The requested module 'express' does not provide an export named 'Response'"
|
|
1138
|
+
// Fix: convert `import { Request, Response } from 'express'` (and variants
|
|
1139
|
+
// with NextFunction etc.) to type-only imports wherever they appear in the
|
|
1140
|
+
// vendored core. This is safe because the core code uses Request/Response
|
|
1141
|
+
// only as type annotations — any runtime `new Request(...)` calls refer
|
|
1142
|
+
// to the global Fetch API Request, not the express one.
|
|
1143
|
+
const expressImportRegex = /^import\s+\{([^}]*)\}\s+from\s+['"]express['"]\s*;?\s*$/gm;
|
|
1144
|
+
const vendoredTsFiles = filesystem.find(coreDir, {
|
|
1145
|
+
matching: '**/*.ts',
|
|
1146
|
+
recursive: true,
|
|
1147
|
+
}) || [];
|
|
1148
|
+
for (const filePath of vendoredTsFiles) {
|
|
1149
|
+
// filesystem.find returns paths relative to jetpack cwd — use absolute resolution
|
|
1150
|
+
const absPath = filePath.startsWith('/') ? filePath : require('node:path').resolve(filePath);
|
|
1151
|
+
if (!filesystem.exists(absPath))
|
|
1152
|
+
continue;
|
|
1153
|
+
const content = filesystem.read(absPath) || '';
|
|
1154
|
+
if (!content.includes("from 'express'") && !content.includes('from "express"'))
|
|
1155
|
+
continue;
|
|
1156
|
+
const patched = content.replace(expressImportRegex, (_match, names) => {
|
|
1157
|
+
const cleanNames = names
|
|
1158
|
+
.split(',')
|
|
1159
|
+
.map((n) => n.trim())
|
|
1160
|
+
.filter((n) => n.length > 0 && n !== 'type')
|
|
1161
|
+
// Strip any pre-existing 'type ' prefix on individual names
|
|
1162
|
+
.map((n) => n.replace(/^type\s+/, ''))
|
|
1163
|
+
.join(', ');
|
|
1164
|
+
return `import type { ${cleanNames} } from 'express';`;
|
|
1165
|
+
});
|
|
1166
|
+
if (patched !== content) {
|
|
1167
|
+
filesystem.write(absPath, patched);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
// ── 4. Rewrite consumer imports: '@lenne.tech/nest-server' → relative ─
|
|
1171
|
+
//
|
|
1172
|
+
// Every .ts file in the starter's src/server/, src/main.ts, tests/,
|
|
1173
|
+
// migrations/, scripts/, migrations-utils/ currently imports from
|
|
1174
|
+
// '@lenne.tech/nest-server'. After vendoring, these must use a
|
|
1175
|
+
// relative path to src/core whose depth depends on the file location.
|
|
1176
|
+
// We handle static imports, dynamic imports, and CJS require() calls.
|
|
1177
|
+
const codemodGlobs = [
|
|
1178
|
+
`${dest}/src/server/**/*.ts`,
|
|
1179
|
+
`${dest}/src/main.ts`,
|
|
1180
|
+
`${dest}/src/config.env.ts`,
|
|
1181
|
+
`${dest}/tests/**/*.ts`,
|
|
1182
|
+
`${dest}/migrations/**/*.ts`,
|
|
1183
|
+
`${dest}/migrations-utils/*.ts`,
|
|
1184
|
+
`${dest}/scripts/**/*.ts`,
|
|
1185
|
+
];
|
|
1186
|
+
tsMorphProject.addSourceFilesAtPaths(codemodGlobs);
|
|
1187
|
+
for (const file of tsMorphProject.getSourceFiles()) {
|
|
1188
|
+
const filePath = file.getFilePath();
|
|
1189
|
+
// Skip files inside src/core/ — those are the framework itself.
|
|
1190
|
+
if (filePath.startsWith(coreDir))
|
|
1191
|
+
continue;
|
|
1192
|
+
const fromDir = path.dirname(filePath);
|
|
1193
|
+
let relToCore = path.relative(fromDir, coreDir).split(path.sep).join('/');
|
|
1194
|
+
if (!relToCore.startsWith('.')) {
|
|
1195
|
+
relToCore = `./${relToCore}`;
|
|
1196
|
+
}
|
|
1197
|
+
let touched = false;
|
|
1198
|
+
// Static import declarations
|
|
1199
|
+
for (const decl of file.getImportDeclarations()) {
|
|
1200
|
+
if (decl.getModuleSpecifierValue() === '@lenne.tech/nest-server') {
|
|
1201
|
+
decl.setModuleSpecifier(relToCore);
|
|
1202
|
+
touched = true;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
// Static export declarations (re-exports)
|
|
1206
|
+
for (const decl of file.getExportDeclarations()) {
|
|
1207
|
+
if (decl.getModuleSpecifierValue() === '@lenne.tech/nest-server') {
|
|
1208
|
+
decl.setModuleSpecifier(relToCore);
|
|
1209
|
+
touched = true;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
// Dynamic imports + CJS require('@lenne.tech/nest-server')
|
|
1213
|
+
file.forEachDescendant((node) => {
|
|
1214
|
+
if (node.getKind() !== SyntaxKind.CallExpression)
|
|
1215
|
+
return;
|
|
1216
|
+
const call = node;
|
|
1217
|
+
const expr = call.getExpression();
|
|
1218
|
+
const exprText = expr.getText();
|
|
1219
|
+
const args = call.getArguments();
|
|
1220
|
+
if (args.length === 0)
|
|
1221
|
+
return;
|
|
1222
|
+
const firstArg = args[0];
|
|
1223
|
+
if (firstArg.getKind() !== SyntaxKind.StringLiteral)
|
|
1224
|
+
return;
|
|
1225
|
+
if (firstArg.getLiteralText() !== '@lenne.tech/nest-server')
|
|
1226
|
+
return;
|
|
1227
|
+
if (exprText === 'require' || exprText === 'import' || expr.getKind() === SyntaxKind.ImportKeyword) {
|
|
1228
|
+
firstArg.replaceWithText(`'${relToCore}'`);
|
|
1229
|
+
touched = true;
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
if (touched) {
|
|
1233
|
+
file.saveSync();
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
// Also patch migrations-utils/*.js (CJS require) via raw string replace
|
|
1237
|
+
// because ts-morph doesn't load .js files in our Project instance.
|
|
1238
|
+
const migrationsUtilsDir = `${dest}/migrations-utils`;
|
|
1239
|
+
if (filesystem.exists(migrationsUtilsDir)) {
|
|
1240
|
+
const jsFiles = filesystem.find(migrationsUtilsDir, { matching: '*.js', recursive: false });
|
|
1241
|
+
for (const f of jsFiles || []) {
|
|
1242
|
+
try {
|
|
1243
|
+
const content = filesystem.read(f);
|
|
1244
|
+
if (content && content.includes('@lenne.tech/nest-server')) {
|
|
1245
|
+
// The migrate helper path is at core/modules/migrate/helpers/migration.helper
|
|
1246
|
+
// regardless of mode, so we replace the package root reference
|
|
1247
|
+
// with the relative path to the vendored core.
|
|
1248
|
+
const fromDir = path.dirname(f);
|
|
1249
|
+
let relToCore = path.relative(fromDir, coreDir).split(path.sep).join('/');
|
|
1250
|
+
if (!relToCore.startsWith('.')) {
|
|
1251
|
+
relToCore = `./${relToCore}`;
|
|
1252
|
+
}
|
|
1253
|
+
const patched = content
|
|
1254
|
+
.replace(/require\(['"]@lenne\.tech\/nest-server['"]\)/g, `require('${relToCore}')`)
|
|
1255
|
+
.replace(/require\(['"]@lenne\.tech\/nest-server\/dist\/([^'"]+)['"]\)/g, (_m, sub) => `require('${relToCore}/${sub}')`);
|
|
1256
|
+
if (patched !== content) {
|
|
1257
|
+
filesystem.write(f, patched);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
catch (_d) {
|
|
1262
|
+
// skip unreadable file
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
// Explicit rewrite of migrations-utils/migrate.js — the generic codemod
|
|
1267
|
+
// above works indirectly (via `bin/migrate.js` loading ts-node first),
|
|
1268
|
+
// but it's fragile: if somebody invokes migrate.js standalone, `require
|
|
1269
|
+
// ('../src/core')` would crash because Node cannot load TypeScript.
|
|
1270
|
+
// Replace the file with the explicit, imo-pilot-proven variant that
|
|
1271
|
+
// registers ts-node itself BEFORE requiring the vendored core and
|
|
1272
|
+
// points at the specific migration.helper path (not the index hub).
|
|
1273
|
+
const migrateJsPath = `${dest}/migrations-utils/migrate.js`;
|
|
1274
|
+
filesystem.write(migrateJsPath, [
|
|
1275
|
+
'// The vendored core is TypeScript-only (no prebuilt dist/). Register ts-node (via our',
|
|
1276
|
+
'// custom bootstrap) before requiring any vendor module, so that .ts source files are',
|
|
1277
|
+
'// transparently compiled on demand. Uses the same compiler config as the migrate CLI.',
|
|
1278
|
+
"require('./ts-compiler');",
|
|
1279
|
+
'',
|
|
1280
|
+
"const { createMigrationStore } = require('../src/core/modules/migrate/helpers/migration.helper');",
|
|
1281
|
+
"const config = require('../src/config.env');",
|
|
1282
|
+
'',
|
|
1283
|
+
'module.exports = createMigrationStore(',
|
|
1284
|
+
' config.default.mongoose.uri,',
|
|
1285
|
+
" 'migrations' // optional, default is 'migrations'",
|
|
1286
|
+
');',
|
|
1287
|
+
'',
|
|
1288
|
+
].join('\n'));
|
|
1289
|
+
// ── 4b. (removed) ─────────────────────────────────────────────────────
|
|
1290
|
+
//
|
|
1291
|
+
// Previous versions copied three maintenance scripts into
|
|
1292
|
+
// `scripts/vendor/` (check-vendor-freshness.mjs, sync-from-upstream.ts,
|
|
1293
|
+
// propose-upstream-pr.ts). The sync + propose scripts duplicated what
|
|
1294
|
+
// the lt-dev Claude Code agents (nest-server-core-updater and
|
|
1295
|
+
// nest-server-core-contributor) do natively — they were dead weight.
|
|
1296
|
+
//
|
|
1297
|
+
// The freshness check is now an inline one-liner in package.json
|
|
1298
|
+
// (see step 5 below). VENDOR.md is the sole vendor-specific file.
|
|
1299
|
+
// ── 5. package.json: remove @lenne.tech/nest-server, add migrate/bin ─
|
|
1300
|
+
//
|
|
1301
|
+
// Delete the framework dep — it's no longer needed since src/core/
|
|
1302
|
+
// carries the code inline. Leave the starter's other deps alone (they
|
|
1303
|
+
// already cover everything `@lenne.tech/nest-server` pulled in
|
|
1304
|
+
// transitively because the starter pins them via pnpm overrides / direct
|
|
1305
|
+
// deps already; see nest-server-starter/package.json).
|
|
1306
|
+
const pkgPath = `${dest}/package.json`;
|
|
1307
|
+
if (filesystem.exists(pkgPath)) {
|
|
1308
|
+
const pkg = filesystem.read(pkgPath, 'json');
|
|
1309
|
+
if (pkg && typeof pkg === 'object') {
|
|
1310
|
+
if (pkg.dependencies && typeof pkg.dependencies === 'object') {
|
|
1311
|
+
delete pkg.dependencies['@lenne.tech/nest-server'];
|
|
1312
|
+
}
|
|
1313
|
+
if (pkg.devDependencies && typeof pkg.devDependencies === 'object') {
|
|
1314
|
+
delete pkg.devDependencies['@lenne.tech/nest-server'];
|
|
1315
|
+
}
|
|
1316
|
+
// Merge the framework's transitive deps into the project's own deps.
|
|
1317
|
+
// The starter lists a minimal subset (express, mongoose, class-validator,
|
|
1318
|
+
// etc.) and previously relied on @lenne.tech/nest-server to pull in
|
|
1319
|
+
// all the other framework dependencies (@apollo/server, @nestjs/jwt,
|
|
1320
|
+
// @nestjs/passport, bcrypt, better-auth, ejs, @tus/server, etc.) as
|
|
1321
|
+
// transitive deps. After vendoring we lose that automatic transitivity,
|
|
1322
|
+
// so we must add those deps explicitly to the project package.json.
|
|
1323
|
+
//
|
|
1324
|
+
// Fully dynamic: we pull in EVERY upstream dependency that the
|
|
1325
|
+
// project doesn't already have. No hard-coded package lists —
|
|
1326
|
+
// additions to upstream's package.json surface automatically on
|
|
1327
|
+
// the next vendor-init or vendor-sync.
|
|
1328
|
+
if (!pkg.dependencies)
|
|
1329
|
+
pkg.dependencies = {};
|
|
1330
|
+
const deps = pkg.dependencies;
|
|
1331
|
+
for (const [depName, depVersion] of Object.entries(upstreamDeps)) {
|
|
1332
|
+
if (depName === '@lenne.tech/nest-server')
|
|
1333
|
+
continue;
|
|
1334
|
+
if (!(depName in deps)) {
|
|
1335
|
+
deps[depName] = depVersion;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
// Merge upstream devDependencies that a consumer project actually
|
|
1339
|
+
// needs at compile time. Rather than hard-coding a list of types
|
|
1340
|
+
// packages (which would drift as upstream evolves), we accept any
|
|
1341
|
+
// `@types/*` package whose base package is present in the merged
|
|
1342
|
+
// runtime deps (e.g. if `bcrypt` is in deps, `@types/bcrypt` is
|
|
1343
|
+
// accepted). This scales to new upstream additions automatically.
|
|
1344
|
+
//
|
|
1345
|
+
// Additionally, any upstream devDependency that the CLI knows is
|
|
1346
|
+
// used at runtime by the framework (via the dedicated
|
|
1347
|
+
// `vendorRuntimeDevDeps` predicate below) is promoted into
|
|
1348
|
+
// `dependencies`, because after vendoring the framework code lives
|
|
1349
|
+
// in the project and needs its runtime helpers available in prod.
|
|
1350
|
+
if (!pkg.devDependencies)
|
|
1351
|
+
pkg.devDependencies = {};
|
|
1352
|
+
const devDeps = pkg.devDependencies;
|
|
1353
|
+
for (const [depName, depVersion] of Object.entries(upstreamDevDeps)) {
|
|
1354
|
+
// Runtime-needed devDeps → promote to dependencies
|
|
1355
|
+
if (this.isVendorRuntimeDep(depName) && !(depName in deps)) {
|
|
1356
|
+
deps[depName] = depVersion;
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
// @types/<pkg> where <pkg> is a runtime dep → accept as devDep
|
|
1360
|
+
if (depName.startsWith('@types/')) {
|
|
1361
|
+
const basePkg = depName.slice('@types/'.length);
|
|
1362
|
+
const matchesRuntime = basePkg in deps ||
|
|
1363
|
+
basePkg === 'node' ||
|
|
1364
|
+
// scoped types: @types/foo__bar ↔ @foo/bar
|
|
1365
|
+
(basePkg.includes('__') && `@${basePkg.replace('__', '/')}` in deps);
|
|
1366
|
+
if (matchesRuntime && !(depName in devDeps) && !(depName in deps)) {
|
|
1367
|
+
devDeps[depName] = depVersion;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
// Add a script to run the local bin/migrate.js. The starter's
|
|
1372
|
+
// existing migrate:* scripts are already correct for npm mode; we
|
|
1373
|
+
// need them pointing at the local bin + local ts-compiler.
|
|
1374
|
+
//
|
|
1375
|
+
// Wire the inline vendor-freshness check and hook it into
|
|
1376
|
+
// `check` / `check:fix` / `check:naf` as a non-blocking first step.
|
|
1377
|
+
if (pkg.scripts && typeof pkg.scripts === 'object') {
|
|
1378
|
+
const scripts = pkg.scripts;
|
|
1379
|
+
const migrateArgs = '--store ./migrations-utils/migrate.js --migrations-dir ./migrations --compiler ts:./migrations-utils/ts-compiler.js';
|
|
1380
|
+
scripts['migrate:create'] =
|
|
1381
|
+
`f() { node ./bin/migrate.js create "$1" --template-file ./src/core/modules/migrate/templates/migration-project.template.ts --migrations-dir ./migrations --compiler ts:./migrations-utils/ts-compiler.js; }; f`;
|
|
1382
|
+
scripts['migrate:up'] = `node ./bin/migrate.js up ${migrateArgs}`;
|
|
1383
|
+
scripts['migrate:down'] = `node ./bin/migrate.js down ${migrateArgs}`;
|
|
1384
|
+
scripts['migrate:list'] = `node ./bin/migrate.js list ${migrateArgs}`;
|
|
1385
|
+
scripts['migrate:develop:up'] = `NODE_ENV=develop node ./bin/migrate.js up ${migrateArgs}`;
|
|
1386
|
+
scripts['migrate:test:up'] = `NODE_ENV=test node ./bin/migrate.js up ${migrateArgs}`;
|
|
1387
|
+
scripts['migrate:preview:up'] = `NODE_ENV=preview node ./bin/migrate.js up ${migrateArgs}`;
|
|
1388
|
+
scripts['migrate:prod:up'] = `NODE_ENV=production node ./bin/migrate.js up ${migrateArgs}`;
|
|
1389
|
+
// Vendor freshness check: reads VENDOR.md baseline version and
|
|
1390
|
+
// compares against npm registry. Non-blocking (always exits 0).
|
|
1391
|
+
// Uses a short inline script — no external file needed.
|
|
1392
|
+
// The heredoc-style approach avoids quote-escaping nightmares.
|
|
1393
|
+
scripts['check:vendor-freshness'] = [
|
|
1394
|
+
'node -e "',
|
|
1395
|
+
"var f=require('fs'),h=require('https');",
|
|
1396
|
+
"try{var c=f.readFileSync('src/core/VENDOR.md','utf8')}catch(e){process.exit(0)}",
|
|
1397
|
+
'var m=c.match(/Baseline-Version[^0-9]*(\\d+\\.\\d+\\.\\d+)/);',
|
|
1398
|
+
'if(!m){process.stderr.write(String.fromCharCode(9888)+\' vendor-freshness: no baseline\\n\');process.exit(0)}',
|
|
1399
|
+
'var v=m[1];',
|
|
1400
|
+
"h.get('https://registry.npmjs.org/@lenne.tech/nest-server/latest',function(r){",
|
|
1401
|
+
"var d='';r.on('data',function(c){d+=c});r.on('end',function(){",
|
|
1402
|
+
"try{var l=JSON.parse(d).version;",
|
|
1403
|
+
"if(v===l)console.log('vendor core up-to-date (v'+v+')');",
|
|
1404
|
+
"else process.stderr.write('vendor core v'+v+', latest v'+l+'\\n')",
|
|
1405
|
+
'}catch(e){}})}).on(\'error\',function(){});',
|
|
1406
|
+
'setTimeout(function(){process.exit(0)},5000)',
|
|
1407
|
+
'"',
|
|
1408
|
+
].join('');
|
|
1409
|
+
// Hook vendor-freshness as the first step of check / check:fix /
|
|
1410
|
+
// check:naf. Non-blocking (exit 0 even on mismatch), so it just
|
|
1411
|
+
// surfaces the warning at the top of the log.
|
|
1412
|
+
const hookFreshness = (scriptName) => {
|
|
1413
|
+
const existing = scripts[scriptName];
|
|
1414
|
+
if (!existing)
|
|
1415
|
+
return;
|
|
1416
|
+
if (existing.includes('check:vendor-freshness'))
|
|
1417
|
+
return;
|
|
1418
|
+
const installPrefix = 'pnpm install && ';
|
|
1419
|
+
if (existing.startsWith(installPrefix)) {
|
|
1420
|
+
scripts[scriptName] = `${installPrefix}pnpm run check:vendor-freshness && ${existing.slice(installPrefix.length)}`;
|
|
1421
|
+
}
|
|
1422
|
+
else {
|
|
1423
|
+
scripts[scriptName] = `pnpm run check:vendor-freshness && ${existing}`;
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1426
|
+
hookFreshness('check');
|
|
1427
|
+
hookFreshness('check:fix');
|
|
1428
|
+
hookFreshness('check:naf');
|
|
1429
|
+
}
|
|
1430
|
+
filesystem.write(pkgPath, pkg, { jsonIndent: 2 });
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
// ── 6. migrations-utils/ts-compiler.js bootstrap ─────────────────────
|
|
1434
|
+
//
|
|
1435
|
+
// The project's tsconfig.json restricts `types` to vitest/globals
|
|
1436
|
+
// which hides Node globals (__dirname, require, exports). This
|
|
1437
|
+
// bootstrap registers ts-node with an explicit Node-aware config.
|
|
1438
|
+
// Always write it fresh in vendor mode — overwrites whatever the
|
|
1439
|
+
// starter shipped (which relies on node_modules/@lenne.tech/nest-server).
|
|
1440
|
+
const tsCompilerPath = `${dest}/migrations-utils/ts-compiler.js`;
|
|
1441
|
+
filesystem.write(tsCompilerPath, [
|
|
1442
|
+
'/**',
|
|
1443
|
+
' * ts-node bootstrap for the migrate CLI (vendor mode).',
|
|
1444
|
+
' *',
|
|
1445
|
+
" * The project's tsconfig.json restricts `types` to vitest/globals",
|
|
1446
|
+
' * which hides Node globals (__dirname, require, exports). This',
|
|
1447
|
+
' * bootstrap registers ts-node with an explicit Node-aware config.',
|
|
1448
|
+
' */',
|
|
1449
|
+
"const tsNode = require('ts-node');",
|
|
1450
|
+
'',
|
|
1451
|
+
'tsNode.register({',
|
|
1452
|
+
' transpileOnly: true,',
|
|
1453
|
+
' compilerOptions: {',
|
|
1454
|
+
" module: 'commonjs',",
|
|
1455
|
+
" target: 'es2022',",
|
|
1456
|
+
' esModuleInterop: true,',
|
|
1457
|
+
' experimentalDecorators: true,',
|
|
1458
|
+
' emitDecoratorMetadata: true,',
|
|
1459
|
+
' skipLibCheck: true,',
|
|
1460
|
+
" types: ['node'],",
|
|
1461
|
+
' },',
|
|
1462
|
+
'});',
|
|
1463
|
+
'',
|
|
1464
|
+
].join('\n'));
|
|
1465
|
+
// ── 6b. extras/sync-packages.mjs: replace with vendor-aware stub ─────
|
|
1466
|
+
//
|
|
1467
|
+
// The starter's `extras/sync-packages.mjs` pulls the latest deps of
|
|
1468
|
+
// `@lenne.tech/nest-server` from the npm registry and merges them into
|
|
1469
|
+
// the project's package.json. In vendor mode the framework is no
|
|
1470
|
+
// longer an npm dependency, so the script has nothing meaningful to
|
|
1471
|
+
// sync and would either no-op or error out.
|
|
1472
|
+
//
|
|
1473
|
+
// Replace it with a small informational stub that points the user at
|
|
1474
|
+
// the canonical vendor-update path (the `nest-server-core-updater`
|
|
1475
|
+
// Claude Code agent). Keeps `pnpm run update` from dead-exiting with
|
|
1476
|
+
// a confusing message.
|
|
1477
|
+
const syncPackagesPath = `${dest}/extras/sync-packages.mjs`;
|
|
1478
|
+
if (filesystem.exists(syncPackagesPath)) {
|
|
1479
|
+
filesystem.write(syncPackagesPath, [
|
|
1480
|
+
'#!/usr/bin/env node',
|
|
1481
|
+
'',
|
|
1482
|
+
"'use strict';",
|
|
1483
|
+
'',
|
|
1484
|
+
'/**',
|
|
1485
|
+
' * Vendor-mode stub for extras/sync-packages.mjs.',
|
|
1486
|
+
' *',
|
|
1487
|
+
' * The original script is designed for npm-mode projects where',
|
|
1488
|
+
" * `@lenne.tech/nest-server` is an installed dependency and",
|
|
1489
|
+
' * `pnpm run update` pulls the latest upstream deps into the',
|
|
1490
|
+
' * project package.json.',
|
|
1491
|
+
' *',
|
|
1492
|
+
' * This project runs in VENDOR mode: the framework core/ tree is',
|
|
1493
|
+
' * copied directly into src/core/ and there is no framework npm',
|
|
1494
|
+
' * dep to sync. To update the vendored core, use the',
|
|
1495
|
+
' * `nest-server-core-updater` Claude Code agent.',
|
|
1496
|
+
' */',
|
|
1497
|
+
'',
|
|
1498
|
+
"console.warn('');",
|
|
1499
|
+
"console.warn('⚠ pnpm run update is a no-op in vendor mode.');",
|
|
1500
|
+
"console.warn(' Framework source lives directly in src/core/ and is updated');",
|
|
1501
|
+
"console.warn(' via the nest-server-core-updater agent:');",
|
|
1502
|
+
"console.warn('');",
|
|
1503
|
+
"console.warn(' /lt-dev:backend:update-nest-server-core');",
|
|
1504
|
+
"console.warn('');",
|
|
1505
|
+
"console.warn(' See src/core/VENDOR.md for the current baseline and sync history.');",
|
|
1506
|
+
"console.warn('');",
|
|
1507
|
+
'process.exit(0);',
|
|
1508
|
+
'',
|
|
1509
|
+
].join('\n'));
|
|
1510
|
+
}
|
|
1511
|
+
// ── 7. tsconfig.json excludes + Node types ───────────────────────────
|
|
1512
|
+
//
|
|
1513
|
+
// The vendored migrate template references `from '@lenne.tech/nest-server'`
|
|
1514
|
+
// as a placeholder that only makes sense in the *generated* migration
|
|
1515
|
+
// file's context. Exclude it from compilation.
|
|
1516
|
+
//
|
|
1517
|
+
// The starter's tsconfig.json restricts `types` to `['vitest/globals']`
|
|
1518
|
+
// because the shipped framework dist was pre-compiled with Node types
|
|
1519
|
+
// baked in. In vendor mode we compile the framework source directly,
|
|
1520
|
+
// so we MUST also load `@types/node` — otherwise types like
|
|
1521
|
+
// `ScryptOptions` in core-better-auth-user.mapper.ts break the build.
|
|
1522
|
+
this.widenTsconfigExcludes(`${dest}/tsconfig.json`);
|
|
1523
|
+
this.widenTsconfigExcludes(`${dest}/tsconfig.build.json`);
|
|
1524
|
+
this.widenTsconfigTypes(`${dest}/tsconfig.json`);
|
|
1525
|
+
this.widenTsconfigTypes(`${dest}/tsconfig.build.json`);
|
|
1526
|
+
// ── 9b. Prepend a vendor-mode block to projects/api/CLAUDE.md ────────
|
|
1527
|
+
//
|
|
1528
|
+
// Claude Code's project-level CLAUDE.md is the single source of truth
|
|
1529
|
+
// for "what kind of project is this". Prepending a short vendor block
|
|
1530
|
+
// tells any downstream agent (backend-dev, generating-nest-servers,
|
|
1531
|
+
// nest-server-updater, …) that the framework lives at src/core/ and
|
|
1532
|
+
// that generated imports must use relative paths, before they even
|
|
1533
|
+
// read the rest of the file.
|
|
1534
|
+
const apiClaudeMdPath = `${dest}/CLAUDE.md`;
|
|
1535
|
+
if (filesystem.exists(apiClaudeMdPath)) {
|
|
1536
|
+
const existing = filesystem.read(apiClaudeMdPath) || '';
|
|
1537
|
+
const marker = '<!-- lt-vendor-marker -->';
|
|
1538
|
+
if (!existing.includes(marker)) {
|
|
1539
|
+
const vendorBlock = [
|
|
1540
|
+
marker,
|
|
1541
|
+
'',
|
|
1542
|
+
'# Vendor-Mode Notice',
|
|
1543
|
+
'',
|
|
1544
|
+
'This api project runs in **vendor mode**: the `@lenne.tech/nest-server`',
|
|
1545
|
+
'core/ tree has been copied directly into `src/core/` as first-class',
|
|
1546
|
+
'project code. There is **no** `@lenne.tech/nest-server` npm dependency.',
|
|
1547
|
+
'',
|
|
1548
|
+
'- **Read framework code from `src/core/**`** — not from `node_modules/`.',
|
|
1549
|
+
'- **Generated imports use relative paths** to `src/core`, e.g.',
|
|
1550
|
+
' `import { CrudService } from \'../../../core\';`',
|
|
1551
|
+
' The exact depth depends on the file location. `lt server module`',
|
|
1552
|
+
' computes it automatically.',
|
|
1553
|
+
'- **Baseline + patch log** live in `src/core/VENDOR.md`. Log any',
|
|
1554
|
+
' substantial local change there so the `nest-server-core-updater`',
|
|
1555
|
+
' agent can classify it at sync time.',
|
|
1556
|
+
'- **Update flow:** run `/lt-dev:backend:update-nest-server-core` (the',
|
|
1557
|
+
' agent clones upstream, computes a delta, and presents a review).',
|
|
1558
|
+
'- **Contribute back:** run `/lt-dev:backend:contribute-nest-server-core`',
|
|
1559
|
+
' to propose local fixes as upstream PRs.',
|
|
1560
|
+
'- **Freshness check:** `pnpm run check:vendor-freshness` warns (non-',
|
|
1561
|
+
' blockingly) when upstream has a newer release than the baseline.',
|
|
1562
|
+
'',
|
|
1563
|
+
'---',
|
|
1564
|
+
'',
|
|
1565
|
+
].join('\n');
|
|
1566
|
+
filesystem.write(apiClaudeMdPath, vendorBlock + existing);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
// ── 9c. Merge nest-server CLAUDE.md sections into project CLAUDE.md ──
|
|
1570
|
+
//
|
|
1571
|
+
// The nest-server CLAUDE.md contains framework-specific instructions for
|
|
1572
|
+
// Claude Code (API conventions, UnifiedField usage, CrudService patterns,
|
|
1573
|
+
// etc.). We merge its H2 sections into the project's CLAUDE.md so that
|
|
1574
|
+
// downstream agents (backend-dev, code-reviewer, nest-server-updater)
|
|
1575
|
+
// have accurate framework knowledge out of the box.
|
|
1576
|
+
//
|
|
1577
|
+
// Merge strategy (matches /lt-dev:fullstack:sync-claude-md):
|
|
1578
|
+
// - Section in upstream but NOT in project → ADD at end
|
|
1579
|
+
// - Section in BOTH → KEEP project version (may have customizations)
|
|
1580
|
+
// - Section only in project → KEEP (project-specific content)
|
|
1581
|
+
if (upstreamClaudeMd && filesystem.exists(apiClaudeMdPath)) {
|
|
1582
|
+
const projectContent = filesystem.read(apiClaudeMdPath) || '';
|
|
1583
|
+
const upstreamSections = this.parseH2Sections(upstreamClaudeMd);
|
|
1584
|
+
const projectSections = this.parseH2Sections(projectContent);
|
|
1585
|
+
const newSections = [];
|
|
1586
|
+
for (const [heading, body] of upstreamSections) {
|
|
1587
|
+
if (!projectSections.has(heading)) {
|
|
1588
|
+
newSections.push(`## ${heading}\n\n${body.trim()}`);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
if (newSections.length > 0) {
|
|
1592
|
+
const separator = projectContent.endsWith('\n') ? '\n' : '\n\n';
|
|
1593
|
+
filesystem.write(apiClaudeMdPath, `${projectContent}${separator}${newSections.join('\n\n')}\n`);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
// ── 10. VENDOR.md baseline ───────────────────────────────────────────
|
|
1597
|
+
//
|
|
1598
|
+
// Record the exact upstream version + commit SHA we vendored from, so
|
|
1599
|
+
// the `nest-server-core-updater` agent has a reliable base for
|
|
1600
|
+
// computing upstream deltas on the next sync.
|
|
1601
|
+
const vendorMdPath = `${dest}/src/core/VENDOR.md`;
|
|
1602
|
+
if (!filesystem.exists(vendorMdPath)) {
|
|
1603
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1604
|
+
const versionLine = upstreamVersion
|
|
1605
|
+
? `- **Baseline-Version:** ${upstreamVersion}`
|
|
1606
|
+
: '- **Baseline-Version:** (not detected — run `/lt-dev:backend:update-nest-server-core` to record)';
|
|
1607
|
+
const commitLine = upstreamCommit
|
|
1608
|
+
? `- **Baseline-Commit:** \`${upstreamCommit}\``
|
|
1609
|
+
: '- **Baseline-Commit:** (not detected)';
|
|
1610
|
+
const syncHistoryTo = upstreamVersion
|
|
1611
|
+
? `${upstreamVersion}${upstreamCommit ? ` (\`${upstreamCommit.slice(0, 10)}\`)` : ''}`
|
|
1612
|
+
: 'initial import';
|
|
1613
|
+
filesystem.write(vendorMdPath, [
|
|
1614
|
+
'# @lenne.tech/nest-server – core (vendored)',
|
|
1615
|
+
'',
|
|
1616
|
+
'This directory is a curated vendor copy of the `core/` tree from',
|
|
1617
|
+
'@lenne.tech/nest-server. It is first-class project code, not a',
|
|
1618
|
+
'node_modules shadow copy. Edit freely; log substantial changes in',
|
|
1619
|
+
'the "Local changes" table below so the `nest-server-core-updater`',
|
|
1620
|
+
'agent can classify them at sync time.',
|
|
1621
|
+
'',
|
|
1622
|
+
'The flatten-fix was applied during `lt fullstack init`: the',
|
|
1623
|
+
'upstream `src/index.ts`, `src/core.module.ts`, `src/test/`,',
|
|
1624
|
+
'`src/templates/`, `src/types/`, and `LICENSE` were moved under',
|
|
1625
|
+
'`src/core/` and their relative `./core/…` specifiers were',
|
|
1626
|
+
'stripped. See the init code in',
|
|
1627
|
+
'`lenneTech/cli/src/extensions/server.ts#convertCloneToVendored`.',
|
|
1628
|
+
'',
|
|
1629
|
+
'## Baseline',
|
|
1630
|
+
'',
|
|
1631
|
+
'- **Upstream-Repo:** https://github.com/lenneTech/nest-server',
|
|
1632
|
+
versionLine,
|
|
1633
|
+
commitLine,
|
|
1634
|
+
`- **Vendored am:** ${today}`,
|
|
1635
|
+
`- **Vendored von:** lt CLI (\`lt fullstack init --framework-mode vendor\`)`,
|
|
1636
|
+
'',
|
|
1637
|
+
'## Sync history',
|
|
1638
|
+
'',
|
|
1639
|
+
'| Date | From | To | Notes |',
|
|
1640
|
+
'| ---- | ---- | -- | ----- |',
|
|
1641
|
+
`| ${today} | — | ${syncHistoryTo} | scaffolded by lt CLI |`,
|
|
1642
|
+
'',
|
|
1643
|
+
'## Local changes',
|
|
1644
|
+
'',
|
|
1645
|
+
'| Date | Commit | Scope | Reason | Status |',
|
|
1646
|
+
'| ---- | ------ | ----- | ------ | ------ |',
|
|
1647
|
+
'| — | — | (none, pristine) | initial vendor | — |',
|
|
1648
|
+
'',
|
|
1649
|
+
'## Upstream PRs',
|
|
1650
|
+
'',
|
|
1651
|
+
'| PR | Title | Commits | Status |',
|
|
1652
|
+
'| -- | ----- | ------- | ------ |',
|
|
1653
|
+
'| — | (none yet) | — | — |',
|
|
1654
|
+
'',
|
|
1655
|
+
].join('\n'));
|
|
1656
|
+
}
|
|
1657
|
+
// ── Post-conversion verification ──────────────────────────────────────
|
|
1658
|
+
//
|
|
1659
|
+
// Scan all consumer files for stale bare-specifier imports that the
|
|
1660
|
+
// codemod should have rewritten. A single miss causes a compile error,
|
|
1661
|
+
// so catching it here with a clear message saves the user debugging time.
|
|
1662
|
+
const staleImports = this.findStaleImports(dest, '@lenne.tech/nest-server');
|
|
1663
|
+
if (staleImports.length > 0) {
|
|
1664
|
+
const { print } = this.toolbox;
|
|
1665
|
+
print.warning(`⚠ ${staleImports.length} file(s) still contain '@lenne.tech/nest-server' imports after vendor conversion:`);
|
|
1666
|
+
for (const f of staleImports.slice(0, 10)) {
|
|
1667
|
+
print.info(` ${f}`);
|
|
1668
|
+
}
|
|
1669
|
+
if (staleImports.length > 10) {
|
|
1670
|
+
print.info(` ... and ${staleImports.length - 10} more`);
|
|
1671
|
+
}
|
|
1672
|
+
print.info('These imports must be manually rewritten to relative paths pointing to src/core.');
|
|
1673
|
+
}
|
|
1674
|
+
return { upstreamDeps, upstreamDevDeps };
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Predicate: is a given upstream `devDependencies` key actually a runtime
|
|
1679
|
+
* dep in disguise that needs to live in `dependencies` after vendoring?
|
|
1680
|
+
*
|
|
1681
|
+
* `@lenne.tech/nest-server` keeps a few packages in devDependencies that
|
|
1682
|
+
* the framework code imports at runtime (e.g. `find-file-up` in its
|
|
1683
|
+
* config loader). When we vendor the framework source into a consumer
|
|
1684
|
+
* project, those must end up in `dependencies` so the compiled/dist
|
|
1685
|
+
* runtime has them available.
|
|
1686
|
+
*
|
|
1687
|
+
* The list of such helpers lives in `src/config/vendor-runtime-deps.json`
|
|
1688
|
+
* under the `runtimeHelpers` key. Adding a new helper is a data-only
|
|
1689
|
+
* change (no CLI release required). If the config file is missing or
|
|
1690
|
+
* unreadable, the predicate safely returns `false` for everything.
|
|
1691
|
+
*/
|
|
1692
|
+
isVendorRuntimeDep(pkgName) {
|
|
1693
|
+
if (!this._vendorRuntimeHelpers) {
|
|
1694
|
+
try {
|
|
1695
|
+
const path = require('path');
|
|
1696
|
+
const configPath = path.join(__dirname, '..', 'config', 'vendor-runtime-deps.json');
|
|
1697
|
+
const raw = this.filesystem.read(configPath, 'json');
|
|
1698
|
+
const list = Array.isArray(raw === null || raw === void 0 ? void 0 : raw.runtimeHelpers) ? raw.runtimeHelpers : [];
|
|
1699
|
+
this._vendorRuntimeHelpers = new Set(list.filter((e) => typeof e === 'string'));
|
|
1700
|
+
}
|
|
1701
|
+
catch (_a) {
|
|
1702
|
+
this._vendorRuntimeHelpers = new Set();
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
return this._vendorRuntimeHelpers.has(pkgName);
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Reads the GraphQL-only package list from the starter's
|
|
1709
|
+
* `api-mode.manifest.json`. Must be called BEFORE `processApiMode`
|
|
1710
|
+
* runs, because `processApiMode` deletes the manifest at the end of
|
|
1711
|
+
* its REST-mode pass.
|
|
1712
|
+
*
|
|
1713
|
+
* Returns a flat list of package names that the manifest declares as
|
|
1714
|
+
* GraphQL-specific (both `packages` and `devPackages` arrays from the
|
|
1715
|
+
* `graphql` mode config). Empty list if the manifest is missing or
|
|
1716
|
+
* malformed.
|
|
1717
|
+
*/
|
|
1718
|
+
readApiModeGraphqlEssentials(dest) {
|
|
1719
|
+
var _a;
|
|
1720
|
+
const manifestPath = `${dest}/api-mode.manifest.json`;
|
|
1721
|
+
if (!this.filesystem.exists(manifestPath))
|
|
1722
|
+
return [];
|
|
1723
|
+
try {
|
|
1724
|
+
const manifest = this.filesystem.read(manifestPath, 'json');
|
|
1725
|
+
const gqlMode = (_a = manifest === null || manifest === void 0 ? void 0 : manifest.modes) === null || _a === void 0 ? void 0 : _a.graphql;
|
|
1726
|
+
if (!gqlMode || typeof gqlMode !== 'object')
|
|
1727
|
+
return [];
|
|
1728
|
+
const packages = [];
|
|
1729
|
+
if (Array.isArray(gqlMode.packages))
|
|
1730
|
+
packages.push(...gqlMode.packages);
|
|
1731
|
+
if (Array.isArray(gqlMode.devPackages))
|
|
1732
|
+
packages.push(...gqlMode.devPackages);
|
|
1733
|
+
return packages.filter((p) => typeof p === 'string' && p.length > 0);
|
|
1734
|
+
}
|
|
1735
|
+
catch (_b) {
|
|
1736
|
+
return [];
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Restores framework-core-essential dependencies that the
|
|
1741
|
+
* `api-mode.manifest.json` of the starter strips when running in
|
|
1742
|
+
* REST-only mode. The vendored framework core at `src/core/` always
|
|
1743
|
+
* imports these packages (e.g. `core-auth.module.ts` imports
|
|
1744
|
+
* `graphql-subscriptions`), so even a pure REST project needs them
|
|
1745
|
+
* available at compile time.
|
|
1746
|
+
*
|
|
1747
|
+
* The essentials list is captured BEFORE `processApiMode` runs (via
|
|
1748
|
+
* `readApiModeGraphqlEssentials`) and passed in here. Versions come
|
|
1749
|
+
* from the upstream `@lenne.tech/nest-server` package.json snapshot
|
|
1750
|
+
* (passed in via `upstreamDeps`). No hard-coded package lists or
|
|
1751
|
+
* versions — drift between starter and upstream is handled automatically
|
|
1752
|
+
* at the next init.
|
|
1753
|
+
*/
|
|
1754
|
+
restoreVendorCoreEssentials(options) {
|
|
1755
|
+
var _a;
|
|
1756
|
+
const { dest, essentials, upstreamDeps = {} } = options;
|
|
1757
|
+
if (!essentials || essentials.length === 0)
|
|
1758
|
+
return;
|
|
1759
|
+
const pkgPath = `${dest}/package.json`;
|
|
1760
|
+
if (!this.filesystem.exists(pkgPath))
|
|
1761
|
+
return;
|
|
1762
|
+
const pkg = this.filesystem.read(pkgPath, 'json');
|
|
1763
|
+
if (!pkg || typeof pkg !== 'object')
|
|
1764
|
+
return;
|
|
1765
|
+
if (!pkg.dependencies)
|
|
1766
|
+
pkg.dependencies = {};
|
|
1767
|
+
const deps = pkg.dependencies;
|
|
1768
|
+
for (const name of essentials) {
|
|
1769
|
+
if (deps[name])
|
|
1770
|
+
continue;
|
|
1771
|
+
// Prefer the upstream-pinned version when available, else leave
|
|
1772
|
+
// unversioned (`latest`) — install will resolve either way.
|
|
1773
|
+
const version = (_a = upstreamDeps[name]) !== null && _a !== void 0 ? _a : 'latest';
|
|
1774
|
+
deps[name] = version;
|
|
1775
|
+
}
|
|
1776
|
+
this.filesystem.write(pkgPath, pkg, { jsonIndent: 2 });
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Ensures the given tsconfig contains both `node` and `vitest/globals`
|
|
1780
|
+
* in its `types` array.
|
|
1781
|
+
*
|
|
1782
|
+
* - `node` is required because the vendored framework source uses Node
|
|
1783
|
+
* globals / types like `ScryptOptions` (from `node:crypto`), which
|
|
1784
|
+
* the starter's compiled dist never needed.
|
|
1785
|
+
* - `vitest/globals` is required because `src/core/test/test.helper.ts`
|
|
1786
|
+
* uses the global `expect` from vitest, and that file is transitively
|
|
1787
|
+
* pulled into the build via TestHelper consumers. tsconfig `exclude`
|
|
1788
|
+
* doesn't help because exclude is a non-transitive filter.
|
|
1789
|
+
*
|
|
1790
|
+
* Idempotent. Handles both bracketed arrays and absent keys, and
|
|
1791
|
+
* leaves the file untouched if both types are already listed.
|
|
1792
|
+
*/
|
|
1793
|
+
widenTsconfigTypes(tsconfigPath) {
|
|
1794
|
+
if (!this.filesystem.exists(tsconfigPath))
|
|
1795
|
+
return;
|
|
1796
|
+
try {
|
|
1797
|
+
let raw = this.filesystem.read(tsconfigPath) || '';
|
|
1798
|
+
const needed = ['node', 'vitest/globals'];
|
|
1799
|
+
for (const type of needed) {
|
|
1800
|
+
// Already contains this type in some types array — skip it.
|
|
1801
|
+
const alreadyRegex = new RegExp(`"types"\\s*:\\s*\\[[^\\]]*"${type.replace(/\//g, '\\/')}"`);
|
|
1802
|
+
if (alreadyRegex.test(raw))
|
|
1803
|
+
continue;
|
|
1804
|
+
const typesRegex = /("types"\s*:\s*\[)([^\]]*)(\])/;
|
|
1805
|
+
if (typesRegex.test(raw)) {
|
|
1806
|
+
raw = raw.replace(typesRegex, (_m, head, body, tail) => {
|
|
1807
|
+
const trimmed = body.trim();
|
|
1808
|
+
const joiner = trimmed.length > 0 ? ', ' : '';
|
|
1809
|
+
return `${head}${body}${joiner}"${type}"${tail}`;
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
else {
|
|
1813
|
+
const compilerOptionsRegex = /("compilerOptions"\s*:\s*\{)/;
|
|
1814
|
+
if (compilerOptionsRegex.test(raw)) {
|
|
1815
|
+
raw = raw.replace(compilerOptionsRegex, `$1\n "types": ["${type}"],`);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
this.filesystem.write(tsconfigPath, raw);
|
|
1820
|
+
}
|
|
1821
|
+
catch (_a) {
|
|
1822
|
+
// best-effort
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Adds the vendored migrate template file AND the vendored `src/core/test/`
|
|
1827
|
+
* directory to the `exclude` array of a tsconfig (build or base).
|
|
1828
|
+
*
|
|
1829
|
+
* - The migrate template file is a text template that references
|
|
1830
|
+
* `@lenne.tech/nest-server` as a placeholder — only meaningful in the
|
|
1831
|
+
* *generated* migration file's context. Exclude from compilation.
|
|
1832
|
+
* - `src/core/test/` contains `TestHelper`, which uses vitest globals
|
|
1833
|
+
* (`expect`, etc.) and is not intended for production dist. The
|
|
1834
|
+
* starter's `tsconfig.build.json` overrides `types` to `["node"]`,
|
|
1835
|
+
* which strips vitest globals; compiling `test.helper.ts` there
|
|
1836
|
+
* breaks. Exclude the whole `src/core/test/` subtree from the build.
|
|
1837
|
+
*
|
|
1838
|
+
* Handles both arrays with pre-existing entries and absent `exclude`
|
|
1839
|
+
* keys. Idempotent.
|
|
1840
|
+
*/
|
|
1841
|
+
widenTsconfigExcludes(tsconfigPath) {
|
|
1842
|
+
if (!this.filesystem.exists(tsconfigPath)) {
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
const EXCLUDE_ENTRIES = [
|
|
1846
|
+
'src/core/modules/migrate/templates/**/*.template.ts',
|
|
1847
|
+
'src/core/test/**/*.ts',
|
|
1848
|
+
];
|
|
1849
|
+
try {
|
|
1850
|
+
// The upstream tsconfig files may contain comments — standard JSON parse
|
|
1851
|
+
// breaks on them. Use a regex-based patch as a fallback.
|
|
1852
|
+
let raw = this.filesystem.read(tsconfigPath) || '';
|
|
1853
|
+
for (const entry of EXCLUDE_ENTRIES) {
|
|
1854
|
+
if (raw.includes(entry))
|
|
1855
|
+
continue;
|
|
1856
|
+
const excludeRegex = /("exclude"\s*:\s*\[)([^\]]*)(\])/;
|
|
1857
|
+
if (excludeRegex.test(raw)) {
|
|
1858
|
+
raw = raw.replace(excludeRegex, (_match, head, body, tail) => {
|
|
1859
|
+
const trimmed = body.trim();
|
|
1860
|
+
const joiner = trimmed.length > 0 ? ', ' : '';
|
|
1861
|
+
return `${head}${body}${joiner}"${entry}"${tail}`;
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
else {
|
|
1865
|
+
// No exclude key at all — add one before the closing brace.
|
|
1866
|
+
const lastBrace = raw.lastIndexOf('}');
|
|
1867
|
+
if (lastBrace === -1)
|
|
1868
|
+
continue;
|
|
1869
|
+
const before = raw.slice(0, lastBrace);
|
|
1870
|
+
const after = raw.slice(lastBrace);
|
|
1871
|
+
const separator = before.trimEnd().endsWith(',') || before.trimEnd().endsWith('{') ? '' : ',';
|
|
1872
|
+
raw = `${before.trimEnd()}${separator}\n "exclude": ["${entry}"]\n${after}`;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
this.filesystem.write(tsconfigPath, raw);
|
|
1876
|
+
}
|
|
1877
|
+
catch (_a) {
|
|
1878
|
+
// Best-effort; the project will still work, it just may need a
|
|
1879
|
+
// manual `tsconfig.json` exclude when building.
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
766
1882
|
/**
|
|
767
1883
|
* Patch config.env.ts using TypeScript AST manipulation
|
|
768
1884
|
* - Replace SECRET_OR_PRIVATE_KEY placeholders with random secrets
|
|
@@ -833,6 +1949,30 @@ class Server {
|
|
|
833
1949
|
}
|
|
834
1950
|
this.filesystem.write(claudeMdPath, content);
|
|
835
1951
|
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Parse a markdown file into a Map of H2 sections.
|
|
1954
|
+
* Key = heading text (without `## `), Value = body text after the heading.
|
|
1955
|
+
* Content before the first H2 heading is stored under key `__preamble__`.
|
|
1956
|
+
*/
|
|
1957
|
+
parseH2Sections(content) {
|
|
1958
|
+
const sections = new Map();
|
|
1959
|
+
const lines = content.split('\n');
|
|
1960
|
+
let currentHeading = '__preamble__';
|
|
1961
|
+
let currentBody = [];
|
|
1962
|
+
for (const line of lines) {
|
|
1963
|
+
const match = /^## (.+)$/.exec(line);
|
|
1964
|
+
if (match) {
|
|
1965
|
+
sections.set(currentHeading, currentBody.join('\n'));
|
|
1966
|
+
currentHeading = match[1].trim();
|
|
1967
|
+
currentBody = [];
|
|
1968
|
+
}
|
|
1969
|
+
else {
|
|
1970
|
+
currentBody.push(line);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
sections.set(currentHeading, currentBody.join('\n'));
|
|
1974
|
+
return sections;
|
|
1975
|
+
}
|
|
836
1976
|
/**
|
|
837
1977
|
* Replace secret or private keys in string (e.g. for config files)
|
|
838
1978
|
*/
|
|
@@ -857,6 +1997,338 @@ class Server {
|
|
|
857
1997
|
return `'${secretMap.get(placeholder)}'`;
|
|
858
1998
|
});
|
|
859
1999
|
}
|
|
2000
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2001
|
+
// Public mode-conversion API (called by `lt server convert-mode`)
|
|
2002
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2003
|
+
/**
|
|
2004
|
+
* Convert an existing npm-mode API project to vendor mode.
|
|
2005
|
+
*
|
|
2006
|
+
* This is a wrapper around {@link convertCloneToVendored} for use on
|
|
2007
|
+
* projects that were **already created** (not during `lt fullstack init`).
|
|
2008
|
+
* The method detects the currently installed `@lenne.tech/nest-server`
|
|
2009
|
+
* version and vendors from that tag unless `upstreamBranch` overrides it.
|
|
2010
|
+
*/
|
|
2011
|
+
convertToVendorMode(options) {
|
|
2012
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2013
|
+
const { dest, upstreamBranch, upstreamRepoUrl } = options;
|
|
2014
|
+
const { isVendoredProject } = require('../lib/framework-detection');
|
|
2015
|
+
if (isVendoredProject(dest)) {
|
|
2016
|
+
throw new Error('Project is already in vendor mode (src/core/VENDOR.md exists).');
|
|
2017
|
+
}
|
|
2018
|
+
// Verify @lenne.tech/nest-server is currently a dependency
|
|
2019
|
+
const pkg = this.filesystem.read(`${dest}/package.json`, 'json');
|
|
2020
|
+
if (!pkg) {
|
|
2021
|
+
throw new Error('Cannot read package.json');
|
|
2022
|
+
}
|
|
2023
|
+
const allDeps = Object.assign(Object.assign({}, (pkg.dependencies || {})), (pkg.devDependencies || {}));
|
|
2024
|
+
if (!allDeps['@lenne.tech/nest-server']) {
|
|
2025
|
+
throw new Error('@lenne.tech/nest-server is not in dependencies or devDependencies. ' +
|
|
2026
|
+
'Is this an npm-mode lenne.tech API project?');
|
|
2027
|
+
}
|
|
2028
|
+
yield this.convertCloneToVendored({
|
|
2029
|
+
dest,
|
|
2030
|
+
upstreamBranch,
|
|
2031
|
+
upstreamRepoUrl,
|
|
2032
|
+
});
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Convert an existing vendor-mode API project back to npm mode.
|
|
2037
|
+
*
|
|
2038
|
+
* Performs the inverse of {@link convertCloneToVendored}:
|
|
2039
|
+
* 1. Read baseline version from VENDOR.md
|
|
2040
|
+
* 2. Delete `src/core/` (the vendored framework)
|
|
2041
|
+
* 3. Rewrite all consumer imports from relative paths back to `@lenne.tech/nest-server`
|
|
2042
|
+
* 4. Restore `@lenne.tech/nest-server` dependency in package.json
|
|
2043
|
+
* 5. Restore migrate scripts to npm paths
|
|
2044
|
+
* 6. Remove vendor-specific scripts and artifacts
|
|
2045
|
+
* 7. Clean up CLAUDE.md vendor marker
|
|
2046
|
+
* 8. Restore tsconfig to npm-mode defaults
|
|
2047
|
+
*/
|
|
2048
|
+
convertToNpmMode(options) {
|
|
2049
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2050
|
+
const { dest, targetVersion } = options;
|
|
2051
|
+
const path = require('path');
|
|
2052
|
+
const { Project, SyntaxKind } = require('ts-morph');
|
|
2053
|
+
const { isVendoredProject } = require('../lib/framework-detection');
|
|
2054
|
+
if (!isVendoredProject(dest)) {
|
|
2055
|
+
throw new Error('Project is not in vendor mode (src/core/VENDOR.md not found).');
|
|
2056
|
+
}
|
|
2057
|
+
const filesystem = this.filesystem;
|
|
2058
|
+
const srcDir = `${dest}/src`;
|
|
2059
|
+
const coreDir = `${srcDir}/core`;
|
|
2060
|
+
// ── 1. Determine target version + warn about local patches ──────────
|
|
2061
|
+
const vendorMd = filesystem.read(`${coreDir}/VENDOR.md`) || '';
|
|
2062
|
+
let version = targetVersion;
|
|
2063
|
+
if (!version) {
|
|
2064
|
+
const match = vendorMd.match(/Baseline-Version:\*{0,2}\s+(\d+\.\d+\.\d+\S*)/);
|
|
2065
|
+
if (match) {
|
|
2066
|
+
version = match[1];
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
if (!version) {
|
|
2070
|
+
throw new Error('Cannot determine target version. Specify --version or ensure VENDOR.md has a Baseline-Version.');
|
|
2071
|
+
}
|
|
2072
|
+
// Warn if VENDOR.md documents local patches that will be lost
|
|
2073
|
+
const localChangesSection = vendorMd.match(/## Local changes[\s\S]*?(?=## |$)/i);
|
|
2074
|
+
if (localChangesSection) {
|
|
2075
|
+
const hasRealPatches = localChangesSection[0].includes('|') &&
|
|
2076
|
+
!localChangesSection[0].includes('(none, pristine)') &&
|
|
2077
|
+
/\|\s*\d{4}-/.test(localChangesSection[0]);
|
|
2078
|
+
if (hasRealPatches) {
|
|
2079
|
+
const { print } = this.toolbox;
|
|
2080
|
+
print.warning('');
|
|
2081
|
+
print.warning('⚠ VENDOR.md documents local patches in src/core/ that will be LOST:');
|
|
2082
|
+
// Extract non-header table rows
|
|
2083
|
+
const rows = localChangesSection[0]
|
|
2084
|
+
.split('\n')
|
|
2085
|
+
.filter((l) => /^\|\s*\d{4}-/.test(l));
|
|
2086
|
+
for (const row of rows.slice(0, 5)) {
|
|
2087
|
+
print.info(` ${row.trim()}`);
|
|
2088
|
+
}
|
|
2089
|
+
if (rows.length > 5) {
|
|
2090
|
+
print.info(` ... and ${rows.length - 5} more`);
|
|
2091
|
+
}
|
|
2092
|
+
print.warning('Consider running /lt-dev:backend:contribute-nest-server-core first to upstream them.');
|
|
2093
|
+
print.warning('');
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
// ── 2. Rewrite consumer imports: relative → @lenne.tech/nest-server ─
|
|
2097
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true });
|
|
2098
|
+
const globs = [
|
|
2099
|
+
`${srcDir}/server/**/*.ts`,
|
|
2100
|
+
`${srcDir}/main.ts`,
|
|
2101
|
+
`${srcDir}/config.env.ts`,
|
|
2102
|
+
`${dest}/tests/**/*.ts`,
|
|
2103
|
+
`${dest}/migrations/**/*.ts`,
|
|
2104
|
+
`${dest}/migrations-utils/*.ts`,
|
|
2105
|
+
`${dest}/scripts/**/*.ts`,
|
|
2106
|
+
];
|
|
2107
|
+
for (const glob of globs) {
|
|
2108
|
+
project.addSourceFilesAtPaths(glob);
|
|
2109
|
+
}
|
|
2110
|
+
const targetSpecifier = '@lenne.tech/nest-server';
|
|
2111
|
+
const coreAbsPath = path.resolve(coreDir);
|
|
2112
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
2113
|
+
let modified = false;
|
|
2114
|
+
// Static imports + re-exports
|
|
2115
|
+
for (const decl of [
|
|
2116
|
+
...sourceFile.getImportDeclarations(),
|
|
2117
|
+
...sourceFile.getExportDeclarations(),
|
|
2118
|
+
]) {
|
|
2119
|
+
const spec = decl.getModuleSpecifierValue();
|
|
2120
|
+
if (!spec)
|
|
2121
|
+
continue;
|
|
2122
|
+
// Check if this import resolves to src/core (the vendored framework)
|
|
2123
|
+
if (this.isVendoredCoreImport(spec, sourceFile.getFilePath(), coreAbsPath)) {
|
|
2124
|
+
decl.setModuleSpecifier(targetSpecifier);
|
|
2125
|
+
modified = true;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
// Dynamic imports + CJS require
|
|
2129
|
+
sourceFile.forEachDescendant((node) => {
|
|
2130
|
+
if (node.getKind() === SyntaxKind.CallExpression) {
|
|
2131
|
+
const expr = node.getExpression().getText();
|
|
2132
|
+
if (expr === 'require' || expr === 'import') {
|
|
2133
|
+
const args = node.getArguments();
|
|
2134
|
+
if (args.length > 0) {
|
|
2135
|
+
const argText = args[0].getText().replace(/['"]/g, '');
|
|
2136
|
+
if (this.isVendoredCoreImport(argText, sourceFile.getFilePath(), coreAbsPath)) {
|
|
2137
|
+
args[0].replaceWithText(`'${targetSpecifier}'`);
|
|
2138
|
+
modified = true;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
if (modified) {
|
|
2145
|
+
sourceFile.saveSync();
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
// Also rewrite .js files in migrations-utils/
|
|
2149
|
+
const jsFiles = filesystem.find(`${dest}/migrations-utils`, { matching: '*.js' }) || [];
|
|
2150
|
+
for (const jsFile of jsFiles) {
|
|
2151
|
+
const content = filesystem.read(jsFile) || '';
|
|
2152
|
+
// Replace any relative require/import that points to src/core
|
|
2153
|
+
const replaced = content.replace(/require\(['"]([^'"]*\/core(?:\/index)?)['"]\)/g, `require('${targetSpecifier}')`);
|
|
2154
|
+
if (replaced !== content) {
|
|
2155
|
+
filesystem.write(jsFile, replaced);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
// ── 3. Delete src/core/ (vendored framework) ────────────────────────
|
|
2159
|
+
filesystem.remove(coreDir);
|
|
2160
|
+
// ── 4. Restore @lenne.tech/nest-server dep + clean package.json ─────
|
|
2161
|
+
const pkg = filesystem.read(`${dest}/package.json`, 'json');
|
|
2162
|
+
if (pkg) {
|
|
2163
|
+
// Add @lenne.tech/nest-server as dependency
|
|
2164
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
2165
|
+
pkg.dependencies['@lenne.tech/nest-server'] = version;
|
|
2166
|
+
// Remove vendor-specific deps that are transitive via nest-server
|
|
2167
|
+
// (they'll come back via node_modules when nest-server is installed)
|
|
2168
|
+
// We only remove deps that are ALSO in nest-server's package.json.
|
|
2169
|
+
// For safety, we don't remove any dep the consumer might use directly.
|
|
2170
|
+
// Remove vendor-specific scripts
|
|
2171
|
+
const scripts = pkg.scripts || {};
|
|
2172
|
+
delete scripts['check:vendor-freshness'];
|
|
2173
|
+
delete scripts['vendor:sync'];
|
|
2174
|
+
delete scripts['vendor:propose-upstream'];
|
|
2175
|
+
// Unhook vendor-freshness from check / check:fix / check:naf
|
|
2176
|
+
for (const key of ['check', 'check:fix', 'check:naf']) {
|
|
2177
|
+
if (scripts[key] && typeof scripts[key] === 'string') {
|
|
2178
|
+
scripts[key] = scripts[key].replace(/pnpm run check:vendor-freshness && /g, '');
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
// Restore migrate scripts to npm-mode paths
|
|
2182
|
+
const migrateCompiler = 'ts:./node_modules/@lenne.tech/nest-server/dist/core/modules/migrate/helpers/ts-compiler.js';
|
|
2183
|
+
const migrateStore = '--store ./migrations-utils/migrate.js --migrations-dir ./migrations';
|
|
2184
|
+
const migrateTemplate = './node_modules/@lenne.tech/nest-server/dist/core/modules/migrate/templates/migration-project.template.ts';
|
|
2185
|
+
scripts['migrate:create'] = `f() { migrate create "$1" --template-file ${migrateTemplate} --migrations-dir ./migrations --compiler ${migrateCompiler}; }; f`;
|
|
2186
|
+
scripts['migrate:up'] = `migrate up ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2187
|
+
scripts['migrate:down'] = `migrate down ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2188
|
+
scripts['migrate:list'] = `migrate list ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2189
|
+
// Env-prefixed migrate scripts
|
|
2190
|
+
for (const env of ['develop', 'test', 'preview', 'prod']) {
|
|
2191
|
+
const nodeEnv = env === 'prod' ? 'production' : env;
|
|
2192
|
+
scripts[`migrate:${env}:up`] = `NODE_ENV=${nodeEnv} migrate up ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2193
|
+
}
|
|
2194
|
+
filesystem.write(`${dest}/package.json`, pkg);
|
|
2195
|
+
}
|
|
2196
|
+
// ── 5. Remove vendor artifacts ──────────────────────────────────────
|
|
2197
|
+
if (filesystem.exists(`${dest}/scripts/vendor`)) {
|
|
2198
|
+
filesystem.remove(`${dest}/scripts/vendor`);
|
|
2199
|
+
}
|
|
2200
|
+
if (filesystem.exists(`${dest}/bin/migrate.js`)) {
|
|
2201
|
+
filesystem.remove(`${dest}/bin/migrate.js`);
|
|
2202
|
+
// Remove bin/ dir if empty
|
|
2203
|
+
const binContents = filesystem.list(`${dest}/bin`) || [];
|
|
2204
|
+
if (binContents.length === 0) {
|
|
2205
|
+
filesystem.remove(`${dest}/bin`);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
if (filesystem.exists(`${dest}/migration-guides`)) {
|
|
2209
|
+
filesystem.remove(`${dest}/migration-guides`);
|
|
2210
|
+
}
|
|
2211
|
+
// Remove migrations-utils/ts-compiler.js (vendor-mode bootstrap)
|
|
2212
|
+
const tsCompilerPath = `${dest}/migrations-utils/ts-compiler.js`;
|
|
2213
|
+
if (filesystem.exists(tsCompilerPath)) {
|
|
2214
|
+
const content = filesystem.read(tsCompilerPath) || '';
|
|
2215
|
+
// Only remove if it's the vendor-mode bootstrap (contains ts-node reference)
|
|
2216
|
+
if (content.includes('ts-node') || content.includes('tsconfig-paths')) {
|
|
2217
|
+
filesystem.remove(tsCompilerPath);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
// Restore extras/sync-packages.mjs if it was replaced with a stub
|
|
2221
|
+
const syncPkgPath = `${dest}/extras/sync-packages.mjs`;
|
|
2222
|
+
if (filesystem.exists(syncPkgPath)) {
|
|
2223
|
+
const content = filesystem.read(syncPkgPath) || '';
|
|
2224
|
+
if (content.includes('vendor mode') || content.includes('vendor:sync')) {
|
|
2225
|
+
// It's the vendor stub — remove it. The user can re-fetch from starter.
|
|
2226
|
+
filesystem.remove(syncPkgPath);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
// ── 6. Clean CLAUDE.md vendor marker ────────────────────────────────
|
|
2230
|
+
const claudeMdPath = `${dest}/CLAUDE.md`;
|
|
2231
|
+
if (filesystem.exists(claudeMdPath)) {
|
|
2232
|
+
let content = filesystem.read(claudeMdPath) || '';
|
|
2233
|
+
const marker = '<!-- lt-vendor-marker -->';
|
|
2234
|
+
if (content.includes(marker)) {
|
|
2235
|
+
// Remove everything from marker to the first `---` separator (end of vendor block)
|
|
2236
|
+
content = content.replace(new RegExp(`${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?---\\s*\\n?`, ''), '');
|
|
2237
|
+
// Remove leading whitespace/newlines
|
|
2238
|
+
content = content.replace(/^\n+/, '');
|
|
2239
|
+
filesystem.write(claudeMdPath, content);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
// ── 7. Restore tsconfig excludes ────────────────────────────────────
|
|
2243
|
+
// tsconfig files use JSONC (comments + trailing commas), so we cannot
|
|
2244
|
+
// use `filesystem.read(path, 'json')` which calls strict `JSON.parse`.
|
|
2245
|
+
// Instead we do a regex-based removal of vendor-specific exclude entries.
|
|
2246
|
+
for (const tsconfigName of ['tsconfig.json', 'tsconfig.build.json']) {
|
|
2247
|
+
const tsconfigPath = `${dest}/${tsconfigName}`;
|
|
2248
|
+
if (!filesystem.exists(tsconfigPath))
|
|
2249
|
+
continue;
|
|
2250
|
+
let raw = filesystem.read(tsconfigPath) || '';
|
|
2251
|
+
// Remove vendor-specific exclude entries (the string literal + optional trailing comma)
|
|
2252
|
+
raw = raw.replace(/,?\s*"src\/core\/modules\/migrate\/templates\/\*\*\/\*\.template\.ts"/g, '');
|
|
2253
|
+
raw = raw.replace(/,?\s*"src\/core\/test\/\*\*\/\*\.ts"/g, '');
|
|
2254
|
+
// Clean up potential double commas or trailing commas before ]
|
|
2255
|
+
raw = raw.replace(/,\s*,/g, ',');
|
|
2256
|
+
raw = raw.replace(/,\s*]/g, ']');
|
|
2257
|
+
filesystem.write(tsconfigPath, raw);
|
|
2258
|
+
}
|
|
2259
|
+
// ── 8. Remove .gitignore vendor entries ──────────────────────────────
|
|
2260
|
+
const gitignorePath = `${dest}/.gitignore`;
|
|
2261
|
+
if (filesystem.exists(gitignorePath)) {
|
|
2262
|
+
let content = filesystem.read(gitignorePath) || '';
|
|
2263
|
+
content = content
|
|
2264
|
+
.split('\n')
|
|
2265
|
+
.filter((line) => !line.includes('scripts/vendor/sync-results') &&
|
|
2266
|
+
!line.includes('scripts/vendor/upstream-candidates'))
|
|
2267
|
+
.join('\n');
|
|
2268
|
+
filesystem.write(gitignorePath, content);
|
|
2269
|
+
}
|
|
2270
|
+
// ── Post-conversion verification ──────────────────────────────────────
|
|
2271
|
+
//
|
|
2272
|
+
// Scan all consumer files for stale relative imports that still resolve
|
|
2273
|
+
// to the (now deleted) src/core/ directory. These would be silent
|
|
2274
|
+
// compile errors.
|
|
2275
|
+
const staleRelativeImports = this.findStaleImports(dest, '../core', /['"]\.\.?\/[^'"]*core['"]|from\s+['"]\.\.?\/[^'"]*core['"]/);
|
|
2276
|
+
if (staleRelativeImports.length > 0) {
|
|
2277
|
+
const { print } = this.toolbox;
|
|
2278
|
+
print.warning(`⚠ ${staleRelativeImports.length} file(s) still contain relative core imports after npm conversion:`);
|
|
2279
|
+
for (const f of staleRelativeImports.slice(0, 10)) {
|
|
2280
|
+
print.info(` ${f}`);
|
|
2281
|
+
}
|
|
2282
|
+
print.info('These imports must be manually rewritten to \'@lenne.tech/nest-server\'.');
|
|
2283
|
+
}
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
/**
|
|
2287
|
+
* Scans consumer source files for import specifiers that should have been
|
|
2288
|
+
* rewritten by a mode conversion. Returns a list of file paths that still
|
|
2289
|
+
* contain matches.
|
|
2290
|
+
*
|
|
2291
|
+
* @param dest Project root directory
|
|
2292
|
+
* @param needle Literal string to search for (used when no regex provided)
|
|
2293
|
+
* @param pattern Optional regex for more flexible matching
|
|
2294
|
+
*/
|
|
2295
|
+
findStaleImports(dest, needle, pattern) {
|
|
2296
|
+
const globs = [
|
|
2297
|
+
`${dest}/src/server/**/*.ts`,
|
|
2298
|
+
`${dest}/src/main.ts`,
|
|
2299
|
+
`${dest}/src/config.env.ts`,
|
|
2300
|
+
`${dest}/tests/**/*.ts`,
|
|
2301
|
+
`${dest}/migrations/**/*.ts`,
|
|
2302
|
+
`${dest}/scripts/**/*.ts`,
|
|
2303
|
+
];
|
|
2304
|
+
const stale = [];
|
|
2305
|
+
for (const glob of globs) {
|
|
2306
|
+
const files = this.filesystem.find(dest, {
|
|
2307
|
+
matching: glob.replace(`${dest}/`, ''),
|
|
2308
|
+
recursive: true,
|
|
2309
|
+
}) || [];
|
|
2310
|
+
for (const file of files) {
|
|
2311
|
+
const content = this.filesystem.read(file) || '';
|
|
2312
|
+
if (pattern ? pattern.test(content) : content.includes(needle)) {
|
|
2313
|
+
stale.push(file.replace(`${dest}/`, ''));
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
return stale;
|
|
2318
|
+
}
|
|
2319
|
+
/**
|
|
2320
|
+
* Checks whether an import specifier resolves to the vendored core directory.
|
|
2321
|
+
* Used during vendor→npm import rewriting.
|
|
2322
|
+
*/
|
|
2323
|
+
isVendoredCoreImport(specifier, fromFilePath, coreAbsPath) {
|
|
2324
|
+
if (!specifier.startsWith('.'))
|
|
2325
|
+
return false;
|
|
2326
|
+
const path = require('path');
|
|
2327
|
+
const resolved = path.resolve(path.dirname(fromFilePath), specifier);
|
|
2328
|
+
// Match if the resolved path IS the core dir or is inside it
|
|
2329
|
+
// (e.g., '../../../core' resolves to src/core, '../../../core/index' resolves to src/core/index)
|
|
2330
|
+
return resolved === coreAbsPath || resolved.startsWith(coreAbsPath + path.sep);
|
|
2331
|
+
}
|
|
860
2332
|
}
|
|
861
2333
|
exports.Server = Server;
|
|
862
2334
|
/**
|