@open-mercato/cli 0.6.2-develop.3467.1.2a1818709d → 0.6.2
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/dist/lib/generators/extensions/notifications.js +14 -4
- package/dist/lib/generators/extensions/notifications.js.map +2 -2
- package/dist/lib/generators/module-registry.js +66 -9
- package/dist/lib/generators/module-registry.js.map +3 -3
- package/dist/lib/testing/integration.js +35 -7
- package/dist/lib/testing/integration.js.map +2 -2
- package/jest.config.cjs +3 -1
- package/package.json +13 -14
- package/src/__tests__/mercato.test.ts +16 -15
- package/src/lib/__tests__/dev-env-reload.test.ts +2 -1
- package/src/lib/__tests__/path-helpers.ts +7 -0
- package/src/lib/db/__tests__/commands.test.ts +2 -1
- package/src/lib/generators/__tests__/module-subset.test.ts +142 -0
- package/src/lib/generators/extensions/notifications.ts +14 -4
- package/src/lib/generators/module-registry.ts +84 -10
- package/src/lib/testing/__tests__/integration.test.ts +28 -5
- package/src/lib/testing/integration.ts +44 -7
package/jest.config.cjs
CHANGED
|
@@ -10,6 +10,8 @@ module.exports = {
|
|
|
10
10
|
{
|
|
11
11
|
tsconfig: {
|
|
12
12
|
jsx: 'react-jsx',
|
|
13
|
+
rootDir: '.',
|
|
14
|
+
ignoreDeprecations: '6.0',
|
|
13
15
|
},
|
|
14
16
|
},
|
|
15
17
|
],
|
|
@@ -21,7 +23,7 @@ module.exports = {
|
|
|
21
23
|
'^@open-mercato/queue/(.*)$': '<rootDir>/../queue/src/$1',
|
|
22
24
|
},
|
|
23
25
|
transformIgnorePatterns: [
|
|
24
|
-
'node_modules/(?!(@mikro-orm)/)',
|
|
26
|
+
'node_modules/(?!(@mikro-orm|kysely)/)',
|
|
25
27
|
],
|
|
26
28
|
testMatch: ['<rootDir>/src/**/__tests__/**/*.test.(ts|tsx)'],
|
|
27
29
|
passWithNoTests: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.6.2
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -55,25 +55,25 @@
|
|
|
55
55
|
"node": "24.x"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@mikro-orm/core": "^7.0.
|
|
59
|
-
"@mikro-orm/decorators": "^7.0.
|
|
60
|
-
"@mikro-orm/migrations": "^7.0.
|
|
61
|
-
"@mikro-orm/postgresql": "^7.0.
|
|
62
|
-
"@open-mercato/queue": "0.6.2
|
|
63
|
-
"@open-mercato/shared": "0.6.2
|
|
58
|
+
"@mikro-orm/core": "^7.0.17",
|
|
59
|
+
"@mikro-orm/decorators": "^7.0.17",
|
|
60
|
+
"@mikro-orm/migrations": "^7.0.17",
|
|
61
|
+
"@mikro-orm/postgresql": "^7.0.17",
|
|
62
|
+
"@open-mercato/queue": "0.6.2",
|
|
63
|
+
"@open-mercato/shared": "0.6.2",
|
|
64
64
|
"cross-spawn": "^7.0.6",
|
|
65
|
-
"pg": "8.
|
|
65
|
+
"pg": "8.21.0",
|
|
66
66
|
"semver": "^7.8.0",
|
|
67
67
|
"testcontainers": "^11.14.0",
|
|
68
68
|
"ts-morph": "^28.0.0",
|
|
69
|
-
"tsx": "^4.
|
|
70
|
-
"typescript": "^
|
|
69
|
+
"tsx": "^4.22.2",
|
|
70
|
+
"typescript": "^6.0.3"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"@open-mercato/shared": "0.6.2
|
|
73
|
+
"@open-mercato/shared": "0.6.2"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@open-mercato/shared": "0.6.2
|
|
76
|
+
"@open-mercato/shared": "0.6.2",
|
|
77
77
|
"@types/jest": "^30.0.0",
|
|
78
78
|
"jest": "^30.4.2",
|
|
79
79
|
"ts-jest": "^29.4.9"
|
|
@@ -85,6 +85,5 @@
|
|
|
85
85
|
"type": "git",
|
|
86
86
|
"url": "https://github.com/open-mercato/open-mercato",
|
|
87
87
|
"directory": "packages/cli"
|
|
88
|
-
}
|
|
89
|
-
"stableVersion": "0.6.1"
|
|
88
|
+
}
|
|
90
89
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
padByCodePointWidth,
|
|
7
7
|
run,
|
|
8
8
|
} from '../mercato'
|
|
9
|
+
import { pathIncludes } from '../lib/__tests__/path-helpers'
|
|
9
10
|
|
|
10
11
|
type MockChildAutoExit = { code: number | null; signal?: NodeJS.Signals | null } | undefined
|
|
11
12
|
type MockChildSpawnRouter = (args: string[]) => MockChildAutoExit
|
|
@@ -674,7 +675,7 @@ describe('server dev managed process exits', () => {
|
|
|
674
675
|
return {
|
|
675
676
|
...actual,
|
|
676
677
|
existsSync: jest.fn((candidate: string) =>
|
|
677
|
-
candidate
|
|
678
|
+
pathIncludes(candidate, 'next/dist/bin/next') || pathIncludes(candidate, '@open-mercato/cli/bin/mercato'),
|
|
678
679
|
),
|
|
679
680
|
unlinkSync: jest.fn(),
|
|
680
681
|
}
|
|
@@ -691,7 +692,7 @@ describe('server dev managed process exits', () => {
|
|
|
691
692
|
}))
|
|
692
693
|
jest.doMock('child_process', () =>
|
|
693
694
|
buildMockChildProcessModule((args) =>
|
|
694
|
-
args[0]
|
|
695
|
+
pathIncludes(args[0] ?? '', 'next/dist/bin/next') ? { code: null, signal: 'SIGTERM' } : undefined,
|
|
695
696
|
),
|
|
696
697
|
)
|
|
697
698
|
|
|
@@ -715,7 +716,7 @@ describe('server dev managed process exits', () => {
|
|
|
715
716
|
return {
|
|
716
717
|
...actual,
|
|
717
718
|
existsSync: jest.fn((candidate: string) =>
|
|
718
|
-
candidate
|
|
719
|
+
pathIncludes(candidate, 'next/dist/bin/next') || pathIncludes(candidate, '@open-mercato/cli/bin/mercato'),
|
|
719
720
|
),
|
|
720
721
|
unlinkSync: jest.fn(),
|
|
721
722
|
}
|
|
@@ -763,7 +764,7 @@ describe('server dev managed process exits', () => {
|
|
|
763
764
|
return {
|
|
764
765
|
...actual,
|
|
765
766
|
existsSync: jest.fn((candidate: string) =>
|
|
766
|
-
candidate
|
|
767
|
+
pathIncludes(candidate, 'next/dist/bin/next') || pathIncludes(candidate, '@open-mercato/cli/bin/mercato'),
|
|
767
768
|
),
|
|
768
769
|
unlinkSync: jest.fn(),
|
|
769
770
|
}
|
|
@@ -792,7 +793,7 @@ describe('server dev managed process exits', () => {
|
|
|
792
793
|
|
|
793
794
|
jest.doMock('child_process', () =>
|
|
794
795
|
buildMockChildProcessModule((args) =>
|
|
795
|
-
args[0]
|
|
796
|
+
pathIncludes(args[0] ?? '', 'next/dist/bin/next') ? { code: null, signal: 'SIGTERM' } : undefined,
|
|
796
797
|
),
|
|
797
798
|
)
|
|
798
799
|
|
|
@@ -830,7 +831,7 @@ describe('server dev managed process exits', () => {
|
|
|
830
831
|
return {
|
|
831
832
|
...actual,
|
|
832
833
|
existsSync: jest.fn((candidate: string) =>
|
|
833
|
-
candidate
|
|
834
|
+
pathIncludes(candidate, 'next/dist/bin/next') || pathIncludes(candidate, '@open-mercato/cli/bin/mercato'),
|
|
834
835
|
),
|
|
835
836
|
unlinkSync: jest.fn(),
|
|
836
837
|
}
|
|
@@ -859,7 +860,7 @@ describe('server dev managed process exits', () => {
|
|
|
859
860
|
|
|
860
861
|
jest.doMock('child_process', () =>
|
|
861
862
|
buildMockChildProcessModule((args) =>
|
|
862
|
-
args[0]
|
|
863
|
+
pathIncludes(args[0] ?? '', 'next/dist/bin/next') ? { code: null, signal: 'SIGTERM' } : undefined,
|
|
863
864
|
),
|
|
864
865
|
)
|
|
865
866
|
|
|
@@ -932,7 +933,7 @@ describe('server start managed process exits', () => {
|
|
|
932
933
|
return {
|
|
933
934
|
...actual,
|
|
934
935
|
existsSync: jest.fn((candidate: string) =>
|
|
935
|
-
candidate
|
|
936
|
+
pathIncludes(candidate, 'next/dist/bin/next') || pathIncludes(candidate, '@open-mercato/cli/bin/mercato'),
|
|
936
937
|
),
|
|
937
938
|
}
|
|
938
939
|
})
|
|
@@ -949,7 +950,7 @@ describe('server start managed process exits', () => {
|
|
|
949
950
|
}))
|
|
950
951
|
jest.doMock('child_process', () =>
|
|
951
952
|
buildMockChildProcessModule((args) =>
|
|
952
|
-
args[0]
|
|
953
|
+
pathIncludes(args[0] ?? '', 'next/dist/bin/next') ? { code: null, signal: 'SIGTERM' } : undefined,
|
|
953
954
|
),
|
|
954
955
|
)
|
|
955
956
|
|
|
@@ -973,7 +974,7 @@ describe('server start managed process exits', () => {
|
|
|
973
974
|
return {
|
|
974
975
|
...actual,
|
|
975
976
|
existsSync: jest.fn((candidate: string) =>
|
|
976
|
-
candidate
|
|
977
|
+
pathIncludes(candidate, 'next/dist/bin/next') || pathIncludes(candidate, '@open-mercato/cli/bin/mercato'),
|
|
977
978
|
),
|
|
978
979
|
}
|
|
979
980
|
})
|
|
@@ -1022,7 +1023,7 @@ describe('server start managed process exits', () => {
|
|
|
1022
1023
|
return {
|
|
1023
1024
|
...actual,
|
|
1024
1025
|
existsSync: jest.fn((candidate: string) =>
|
|
1025
|
-
candidate
|
|
1026
|
+
pathIncludes(candidate, 'next/dist/bin/next') || pathIncludes(candidate, '@open-mercato/cli/bin/mercato'),
|
|
1026
1027
|
),
|
|
1027
1028
|
unlinkSync: jest.fn(),
|
|
1028
1029
|
}
|
|
@@ -1089,7 +1090,7 @@ describe('server start managed process exits', () => {
|
|
|
1089
1090
|
|
|
1090
1091
|
return {
|
|
1091
1092
|
spawn: jest.fn((_command: string, args: string[]) => {
|
|
1092
|
-
if (args[0]
|
|
1093
|
+
if (pathIncludes(args[0] ?? '', 'next/dist/bin/next')) {
|
|
1093
1094
|
nextSpawnCount += 1
|
|
1094
1095
|
if (nextSpawnCount === 1) {
|
|
1095
1096
|
queueMicrotask(() => envChangeCallback?.('/tmp/test-app/.env'))
|
|
@@ -1106,7 +1107,7 @@ describe('server start managed process exits', () => {
|
|
|
1106
1107
|
const exitCode = await mercado.run(['node', 'mercato', 'server', 'dev'])
|
|
1107
1108
|
const { spawn } = await import('child_process')
|
|
1108
1109
|
const nextSpawns = (spawn as jest.Mock).mock.calls.filter((call) =>
|
|
1109
|
-
call[1]?.[0]
|
|
1110
|
+
pathIncludes(call[1]?.[0] ?? '', 'next/dist/bin/next'),
|
|
1110
1111
|
)
|
|
1111
1112
|
|
|
1112
1113
|
expect(exitCode).toBe(0)
|
|
@@ -1132,7 +1133,7 @@ describe('server start managed process exits', () => {
|
|
|
1132
1133
|
return {
|
|
1133
1134
|
...actual,
|
|
1134
1135
|
existsSync: jest.fn((candidate: string) =>
|
|
1135
|
-
candidate
|
|
1136
|
+
pathIncludes(candidate, 'next/dist/bin/next') || pathIncludes(candidate, '@open-mercato/cli/bin/mercato'),
|
|
1136
1137
|
),
|
|
1137
1138
|
}
|
|
1138
1139
|
})
|
|
@@ -1159,7 +1160,7 @@ describe('server start managed process exits', () => {
|
|
|
1159
1160
|
|
|
1160
1161
|
jest.doMock('child_process', () =>
|
|
1161
1162
|
buildMockChildProcessModule((args) =>
|
|
1162
|
-
args[0]
|
|
1163
|
+
pathIncludes(args[0] ?? '', 'next/dist/bin/next') ? { code: null, signal: 'SIGTERM' } : undefined,
|
|
1163
1164
|
),
|
|
1164
1165
|
)
|
|
1165
1166
|
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs'
|
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { createDevEnvReloader, resolveDevEnvFilePaths } from '../dev-env-reload'
|
|
5
|
+
import { normalizeTestPath } from './path-helpers'
|
|
5
6
|
|
|
6
7
|
describe('dev env reload helpers', () => {
|
|
7
8
|
let appDir: string
|
|
@@ -15,7 +16,7 @@ describe('dev env reload helpers', () => {
|
|
|
15
16
|
})
|
|
16
17
|
|
|
17
18
|
it('resolves app env files in low-to-high dev precedence order', () => {
|
|
18
|
-
expect(resolveDevEnvFilePaths('/tmp/app')).toEqual([
|
|
19
|
+
expect(resolveDevEnvFilePaths('/tmp/app').map(normalizeTestPath)).toEqual([
|
|
19
20
|
'/tmp/app/.env',
|
|
20
21
|
'/tmp/app/.env.development',
|
|
21
22
|
'/tmp/app/.env.local',
|
|
@@ -13,6 +13,7 @@ import fs from 'node:fs'
|
|
|
13
13
|
import os from 'node:os'
|
|
14
14
|
import path from 'node:path'
|
|
15
15
|
import type { ModuleEntry, PackageResolver } from '../../resolver'
|
|
16
|
+
import { normalizeTestPath } from '../../__tests__/path-helpers'
|
|
16
17
|
|
|
17
18
|
describe('db commands security', () => {
|
|
18
19
|
describe('sanitizeModuleId', () => {
|
|
@@ -189,7 +190,7 @@ describe('resolveGeneratedMigrationPath', () => {
|
|
|
189
190
|
'/repo/src/modules/example/migrations',
|
|
190
191
|
)
|
|
191
192
|
|
|
192
|
-
expect(result).toBe('/repo/src/modules/example/migrations/Migration20260506101927.ts')
|
|
193
|
+
expect(normalizeTestPath(result)).toBe('/repo/src/modules/example/migrations/Migration20260506101927.ts')
|
|
193
194
|
})
|
|
194
195
|
|
|
195
196
|
it('keeps absolute MikroORM results unchanged', () => {
|
|
@@ -768,6 +768,148 @@ export function GET() {
|
|
|
768
768
|
)
|
|
769
769
|
})
|
|
770
770
|
|
|
771
|
+
it('emits imports for third-party scoped npm packages registered via src/modules.ts', async () => {
|
|
772
|
+
// Simulate a third-party module package published at @dainamite/cpq
|
|
773
|
+
// (see issue #1998). The generator must accept @<vendor>/<package> specifiers
|
|
774
|
+
// and emit imports that point at the package's modules/<id> subtree.
|
|
775
|
+
const pkgModuleBase = path.join(
|
|
776
|
+
tmpDir,
|
|
777
|
+
'packages',
|
|
778
|
+
'thirdparty',
|
|
779
|
+
'src',
|
|
780
|
+
'modules',
|
|
781
|
+
'cpq',
|
|
782
|
+
)
|
|
783
|
+
touchFile(
|
|
784
|
+
path.join(pkgModuleBase, 'subscribers', 'on-event.ts'),
|
|
785
|
+
`export const metadata = { event: 'cpq.quote.created' }\nexport default async function handler() {}\n`,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
const baseResolver = createMockResolver(tmpDir, [
|
|
789
|
+
{ id: 'cpq', from: '@dainamite/cpq' },
|
|
790
|
+
])
|
|
791
|
+
const resolver: PackageResolver = {
|
|
792
|
+
...baseResolver,
|
|
793
|
+
getModulePaths: (entry: ModuleEntry) => ({
|
|
794
|
+
appBase: path.join(tmpDir, 'app', 'src', 'modules', entry.id),
|
|
795
|
+
pkgBase: pkgModuleBase,
|
|
796
|
+
}),
|
|
797
|
+
getModuleImportBase: (entry: ModuleEntry) => ({
|
|
798
|
+
appBase: `@/modules/${entry.id}`,
|
|
799
|
+
pkgBase: `@dainamite/cpq/modules/${entry.id}`,
|
|
800
|
+
}),
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const result = await generateModuleRegistry({ resolver, quiet: true })
|
|
804
|
+
expect(result.errors).toEqual([])
|
|
805
|
+
|
|
806
|
+
const output = readGenerated(tmpDir, 'modules.generated.ts')!
|
|
807
|
+
expect(output).toContain('id: "cpq"')
|
|
808
|
+
expect(output).toContain('@dainamite/cpq/modules/cpq/subscribers/on-event')
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
it('emits imports for bare (unscoped) third-party npm packages', async () => {
|
|
812
|
+
const pkgModuleBase = path.join(
|
|
813
|
+
tmpDir,
|
|
814
|
+
'packages',
|
|
815
|
+
'thirdparty-bare',
|
|
816
|
+
'src',
|
|
817
|
+
'modules',
|
|
818
|
+
'foo',
|
|
819
|
+
)
|
|
820
|
+
touchFile(
|
|
821
|
+
path.join(pkgModuleBase, 'subscribers', 'on-event.ts'),
|
|
822
|
+
`export const metadata = { event: 'foo.thing.created' }\nexport default async function handler() {}\n`,
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
const baseResolver = createMockResolver(tmpDir, [
|
|
826
|
+
{ id: 'foo', from: 'acme-modules' },
|
|
827
|
+
])
|
|
828
|
+
const resolver: PackageResolver = {
|
|
829
|
+
...baseResolver,
|
|
830
|
+
getModulePaths: (entry: ModuleEntry) => ({
|
|
831
|
+
appBase: path.join(tmpDir, 'app', 'src', 'modules', entry.id),
|
|
832
|
+
pkgBase: pkgModuleBase,
|
|
833
|
+
}),
|
|
834
|
+
getModuleImportBase: (entry: ModuleEntry) => ({
|
|
835
|
+
appBase: `@/modules/${entry.id}`,
|
|
836
|
+
pkgBase: `acme-modules/modules/${entry.id}`,
|
|
837
|
+
}),
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const result = await generateModuleRegistry({ resolver, quiet: true })
|
|
841
|
+
expect(result.errors).toEqual([])
|
|
842
|
+
|
|
843
|
+
const output = readGenerated(tmpDir, 'modules.generated.ts')!
|
|
844
|
+
expect(output).toContain('id: "foo"')
|
|
845
|
+
expect(output).toContain('acme-modules/modules/foo/subscribers/on-event')
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
it('fails fast when a third-party module entry points at a package without the expected OM module shape', async () => {
|
|
849
|
+
// No files scaffolded — the package directory does not exist, so the
|
|
850
|
+
// entry is almost certainly not an Open Mercato module.
|
|
851
|
+
const enabled: ModuleEntry[] = [{ id: 'cpq', from: '@dainamite/cpq' }]
|
|
852
|
+
const baseResolver = createMockResolver(tmpDir, enabled)
|
|
853
|
+
const resolver: PackageResolver = {
|
|
854
|
+
...baseResolver,
|
|
855
|
+
getModulePaths: (entry: ModuleEntry) => ({
|
|
856
|
+
appBase: path.join(tmpDir, 'app', 'src', 'modules', entry.id),
|
|
857
|
+
pkgBase: path.join(
|
|
858
|
+
tmpDir,
|
|
859
|
+
'node_modules',
|
|
860
|
+
'@dainamite',
|
|
861
|
+
'cpq',
|
|
862
|
+
'dist',
|
|
863
|
+
'modules',
|
|
864
|
+
entry.id,
|
|
865
|
+
),
|
|
866
|
+
}),
|
|
867
|
+
getModuleImportBase: (entry: ModuleEntry) => ({
|
|
868
|
+
appBase: `@/modules/${entry.id}`,
|
|
869
|
+
pkgBase: `@dainamite/cpq/modules/${entry.id}`,
|
|
870
|
+
}),
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
await expect(generateModuleRegistry({ resolver, quiet: true })).rejects.toThrow(
|
|
874
|
+
/no Open Mercato module shape was found/,
|
|
875
|
+
)
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
it('rejects third-party specifiers with path-traversal segments', async () => {
|
|
879
|
+
const pkgModuleBase = path.join(
|
|
880
|
+
tmpDir,
|
|
881
|
+
'packages',
|
|
882
|
+
'thirdparty-traversal',
|
|
883
|
+
'src',
|
|
884
|
+
'modules',
|
|
885
|
+
'evil',
|
|
886
|
+
)
|
|
887
|
+
touchFile(
|
|
888
|
+
path.join(pkgModuleBase, 'subscribers', 'on-event.ts'),
|
|
889
|
+
`export const metadata = { event: 'evil.x' }\nexport default async function handler() {}\n`,
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
const baseResolver = createMockResolver(tmpDir, [
|
|
893
|
+
{ id: 'evil', from: '@vendor/pkg' },
|
|
894
|
+
])
|
|
895
|
+
const resolver: PackageResolver = {
|
|
896
|
+
...baseResolver,
|
|
897
|
+
getModulePaths: (entry: ModuleEntry) => ({
|
|
898
|
+
appBase: path.join(tmpDir, 'app', 'src', 'modules', entry.id),
|
|
899
|
+
pkgBase: pkgModuleBase,
|
|
900
|
+
}),
|
|
901
|
+
// Inject a malicious specifier with a ".." segment.
|
|
902
|
+
getModuleImportBase: (entry: ModuleEntry) => ({
|
|
903
|
+
appBase: `@/modules/${entry.id}`,
|
|
904
|
+
pkgBase: `@vendor/pkg/../escape/modules/${entry.id}`,
|
|
905
|
+
}),
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
await expect(generateModuleRegistry({ resolver, quiet: true })).rejects.toThrow(
|
|
909
|
+
/Unsafe generated module specifier/,
|
|
910
|
+
)
|
|
911
|
+
})
|
|
912
|
+
|
|
771
913
|
it('mixed subset: core module + app module, then remove core module', async () => {
|
|
772
914
|
scaffoldModule(tmpDir, 'core_mod', 'pkg', [
|
|
773
915
|
'backend/page.tsx',
|
|
@@ -123,6 +123,7 @@ export function createNotificationsExtension(): GeneratorExtension {
|
|
|
123
123
|
fileName: 'notifications.generated.ts',
|
|
124
124
|
imports: [
|
|
125
125
|
{ namedImports: [{ name: 'NotificationTypeDefinition', isTypeOnly: true }], moduleSpecifier: '@open-mercato/shared/modules/notifications/types' },
|
|
126
|
+
{ namedImports: [{ name: 'applyNotificationTypeOverridesToEntries' }], moduleSpecifier: '@open-mercato/shared/modules/overrides' },
|
|
126
127
|
...notificationImports,
|
|
127
128
|
],
|
|
128
129
|
build(sourceFile) {
|
|
@@ -138,9 +139,13 @@ export function createNotificationsExtension(): GeneratorExtension {
|
|
|
138
139
|
type: 'NotificationTypeEntry[]',
|
|
139
140
|
initializer: arrayLiteral(notificationTypeEntries, writeValue),
|
|
140
141
|
},
|
|
142
|
+
{
|
|
143
|
+
name: 'entries',
|
|
144
|
+
initializer: callExpression(identifier('applyNotificationTypeOverridesToEntries'), [identifier('entriesRaw')]),
|
|
145
|
+
},
|
|
141
146
|
{
|
|
142
147
|
name: 'allTypes',
|
|
143
|
-
initializer: methodCall(identifier('
|
|
148
|
+
initializer: methodCall(identifier('entries'), 'flatMap', [
|
|
144
149
|
arrowFunction({
|
|
145
150
|
parameters: ['entry'],
|
|
146
151
|
body: propertyAccess(identifier('entry'), 'types'),
|
|
@@ -153,7 +158,7 @@ export function createNotificationsExtension(): GeneratorExtension {
|
|
|
153
158
|
declarationKind: VariableDeclarationKind.Const,
|
|
154
159
|
isExported: true,
|
|
155
160
|
declarations: [
|
|
156
|
-
{ name: 'notificationTypeEntries', initializer: identifier('
|
|
161
|
+
{ name: 'notificationTypeEntries', initializer: identifier('entries') },
|
|
157
162
|
{ name: 'notificationTypes', initializer: identifier('allTypes') },
|
|
158
163
|
],
|
|
159
164
|
})
|
|
@@ -193,6 +198,7 @@ export function createNotificationsExtension(): GeneratorExtension {
|
|
|
193
198
|
],
|
|
194
199
|
moduleSpecifier: '@open-mercato/shared/modules/notifications/types',
|
|
195
200
|
},
|
|
201
|
+
{ namedImports: [{ name: 'applyNotificationTypeOverridesToEntries' }], moduleSpecifier: '@open-mercato/shared/modules/overrides' },
|
|
196
202
|
...notificationClientImports,
|
|
197
203
|
],
|
|
198
204
|
build(sourceFile) {
|
|
@@ -213,9 +219,13 @@ export function createNotificationsExtension(): GeneratorExtension {
|
|
|
213
219
|
type: 'NotificationTypeEntry[]',
|
|
214
220
|
initializer: arrayLiteral(notificationClientTypeEntries, writeValue),
|
|
215
221
|
},
|
|
222
|
+
{
|
|
223
|
+
name: 'entries',
|
|
224
|
+
initializer: callExpression(identifier('applyNotificationTypeOverridesToEntries'), [identifier('entriesRaw')]),
|
|
225
|
+
},
|
|
216
226
|
{
|
|
217
227
|
name: 'allTypes',
|
|
218
|
-
initializer: methodCall(identifier('
|
|
228
|
+
initializer: methodCall(identifier('entries'), 'flatMap', [
|
|
219
229
|
arrowFunction({
|
|
220
230
|
parameters: ['entry'],
|
|
221
231
|
body: propertyAccess(identifier('entry'), 'types'),
|
|
@@ -255,7 +265,7 @@ export function createNotificationsExtension(): GeneratorExtension {
|
|
|
255
265
|
declarationKind: VariableDeclarationKind.Const,
|
|
256
266
|
isExported: true,
|
|
257
267
|
declarations: [
|
|
258
|
-
{ name: 'notificationClientTypeEntries', initializer: identifier('
|
|
268
|
+
{ name: 'notificationClientTypeEntries', initializer: identifier('entries') },
|
|
259
269
|
{ name: 'notificationClientTypes', initializer: identifier('allTypes') },
|
|
260
270
|
{ name: 'notificationRenderers', initializer: identifier('renderers') },
|
|
261
271
|
],
|
|
@@ -925,8 +925,12 @@ function toLiteral(value: unknown): string {
|
|
|
925
925
|
.replace(/\u2029/g, '\\u2029')
|
|
926
926
|
}
|
|
927
927
|
|
|
928
|
-
const
|
|
928
|
+
const GENERATED_MODULE_RELATIVE_PREFIXES = ['@/', '../../src/modules/', './'] as const
|
|
929
929
|
const GENERATED_MODULE_SPECIFIER_SEGMENT = /^[A-Za-z0-9_.\-[\]()']+$/
|
|
930
|
+
// Lenient npm package name matcher. Accepts legacy uppercase packages and
|
|
931
|
+
// tilde-prefixed names, bans leading "." / "_", and reserves "@" for scope
|
|
932
|
+
// markers so the dispatcher below can split scope-vs-bare unambiguously.
|
|
933
|
+
const NPM_PACKAGE_NAME_SEGMENT = /^[A-Za-z0-9~][A-Za-z0-9_.~-]*$/
|
|
930
934
|
|
|
931
935
|
function sanitizeGeneratedModuleSpecifierSegment(segment: string, importPath: string): string {
|
|
932
936
|
const match = GENERATED_MODULE_SPECIFIER_SEGMENT.exec(segment)
|
|
@@ -936,22 +940,83 @@ function sanitizeGeneratedModuleSpecifierSegment(segment: string, importPath: st
|
|
|
936
940
|
return match[0]
|
|
937
941
|
}
|
|
938
942
|
|
|
939
|
-
function
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
throw new Error(`Unsafe generated module specifier prefix: ${importPath}`)
|
|
943
|
+
function sanitizeNpmPackageNameSegment(segment: string, importPath: string): void {
|
|
944
|
+
if (!NPM_PACKAGE_NAME_SEGMENT.test(segment) || segment === '.' || segment === '..') {
|
|
945
|
+
throw new Error(`Unsafe generated module specifier: ${importPath}`)
|
|
943
946
|
}
|
|
947
|
+
}
|
|
944
948
|
|
|
945
|
-
|
|
946
|
-
if (!
|
|
949
|
+
function sanitizeGeneratedModuleSpecifier(importPath: string): string {
|
|
950
|
+
if (!importPath) {
|
|
947
951
|
throw new Error(`Unsafe generated module specifier: ${importPath}`)
|
|
948
952
|
}
|
|
949
953
|
|
|
950
|
-
const
|
|
954
|
+
const relativePrefix = GENERATED_MODULE_RELATIVE_PREFIXES.find((candidate) =>
|
|
955
|
+
importPath.startsWith(candidate),
|
|
956
|
+
)
|
|
957
|
+
if (relativePrefix) {
|
|
958
|
+
const suffix = importPath.slice(relativePrefix.length)
|
|
959
|
+
if (!suffix) {
|
|
960
|
+
throw new Error(`Unsafe generated module specifier: ${importPath}`)
|
|
961
|
+
}
|
|
962
|
+
const segments = suffix
|
|
963
|
+
.split('/')
|
|
964
|
+
.map((segment) => sanitizeGeneratedModuleSpecifierSegment(segment, importPath))
|
|
965
|
+
return `${relativePrefix}${segments.join('/')}`
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (importPath.startsWith('@')) {
|
|
969
|
+
// Scoped npm package: @scope/name[/subpath]
|
|
970
|
+
const firstSlash = importPath.indexOf('/')
|
|
971
|
+
if (firstSlash < 2) {
|
|
972
|
+
throw new Error(`Unsafe generated module specifier prefix: ${importPath}`)
|
|
973
|
+
}
|
|
974
|
+
const scope = importPath.slice(1, firstSlash)
|
|
975
|
+
sanitizeNpmPackageNameSegment(scope, importPath)
|
|
976
|
+
const rest = importPath.slice(firstSlash + 1)
|
|
977
|
+
if (!rest) {
|
|
978
|
+
throw new Error(`Unsafe generated module specifier: ${importPath}`)
|
|
979
|
+
}
|
|
980
|
+
const secondSlash = rest.indexOf('/')
|
|
981
|
+
const name = secondSlash >= 0 ? rest.slice(0, secondSlash) : rest
|
|
982
|
+
sanitizeNpmPackageNameSegment(name, importPath)
|
|
983
|
+
const subpath = secondSlash >= 0 ? rest.slice(secondSlash + 1) : ''
|
|
984
|
+
if (!subpath) {
|
|
985
|
+
return `@${scope}/${name}`
|
|
986
|
+
}
|
|
987
|
+
const segments = subpath
|
|
988
|
+
.split('/')
|
|
989
|
+
.map((segment) => sanitizeGeneratedModuleSpecifierSegment(segment, importPath))
|
|
990
|
+
return `@${scope}/${name}/${segments.join('/')}`
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Bare npm package: name[/subpath]
|
|
994
|
+
const firstSlash = importPath.indexOf('/')
|
|
995
|
+
const name = firstSlash >= 0 ? importPath.slice(0, firstSlash) : importPath
|
|
996
|
+
sanitizeNpmPackageNameSegment(name, importPath)
|
|
997
|
+
const subpath = firstSlash >= 0 ? importPath.slice(firstSlash + 1) : ''
|
|
998
|
+
if (!subpath) {
|
|
999
|
+
return name
|
|
1000
|
+
}
|
|
1001
|
+
const segments = subpath
|
|
951
1002
|
.split('/')
|
|
952
1003
|
.map((segment) => sanitizeGeneratedModuleSpecifierSegment(segment, importPath))
|
|
953
|
-
|
|
954
|
-
|
|
1004
|
+
return `${name}/${segments.join('/')}`
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Defense in depth: when src/modules.ts registers a module from a third-party
|
|
1008
|
+
// npm scope (anything outside @app / @open-mercato/*), verify the resolved
|
|
1009
|
+
// pkgBase directory exists. If it doesn't, the package referenced via `from:`
|
|
1010
|
+
// is almost certainly not an Open Mercato module (or the id is wrong), and
|
|
1011
|
+
// silently emitting an empty module registration would hide the misconfig.
|
|
1012
|
+
function verifyThirdPartyModuleShape(entry: { id: string; from?: string }, pkgBase: string): void {
|
|
1013
|
+
const from = entry.from
|
|
1014
|
+
if (!from || from === '@app' || from.startsWith('@open-mercato/')) return
|
|
1015
|
+
if (fs.existsSync(pkgBase)) return
|
|
1016
|
+
throw new Error(
|
|
1017
|
+
`Module '${entry.id}' is registered with from: '${from}' but no Open Mercato module shape was found at '${pkgBase}'. ` +
|
|
1018
|
+
`Ensure the package ships a 'modules/${entry.id}' subtree (either under 'dist/modules/' or 'src/modules/'), or correct the 'from' value in src/modules.ts.`,
|
|
1019
|
+
)
|
|
955
1020
|
}
|
|
956
1021
|
|
|
957
1022
|
function buildImportStatement(importClause: string, importPath: string): string {
|
|
@@ -2502,6 +2567,9 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
|
|
|
2502
2567
|
const legacySubscribersChecksumFile = path.join(outputDir, 'subscribers.generated.checksum')
|
|
2503
2568
|
|
|
2504
2569
|
const enabled = resolver.loadEnabledModules()
|
|
2570
|
+
for (const entry of enabled) {
|
|
2571
|
+
verifyThirdPartyModuleShape(entry, resolver.getModulePaths(entry).pkgBase)
|
|
2572
|
+
}
|
|
2505
2573
|
const extensions = loadGeneratorExtensions()
|
|
2506
2574
|
|
|
2507
2575
|
// Pre-pass: collect generator plugins from each enabled module's generators.ts
|
|
@@ -3118,6 +3186,9 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
|
|
|
3118
3186
|
const enabledIdsChecksumFile = path.join(outputDir, 'enabled-module-ids.generated.checksum')
|
|
3119
3187
|
|
|
3120
3188
|
const enabled = resolver.loadEnabledModules()
|
|
3189
|
+
for (const entry of enabled) {
|
|
3190
|
+
verifyThirdPartyModuleShape(entry, resolver.getModulePaths(entry).pkgBase)
|
|
3191
|
+
}
|
|
3121
3192
|
const imports: string[] = []
|
|
3122
3193
|
const moduleDecls: WriterFunction[] = []
|
|
3123
3194
|
const importIdRef = { value: 0 }
|
|
@@ -3449,6 +3520,9 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
|
|
|
3449
3520
|
const legacyChecksumFile = path.join(outputDir, 'cli-modules.generated.checksum')
|
|
3450
3521
|
|
|
3451
3522
|
const enabled = resolver.loadEnabledModules()
|
|
3523
|
+
for (const entry of enabled) {
|
|
3524
|
+
verifyThirdPartyModuleShape(entry, resolver.getModulePaths(entry).pkgBase)
|
|
3525
|
+
}
|
|
3452
3526
|
const imports: string[] = []
|
|
3453
3527
|
const moduleDecls: WriterFunction[] = []
|
|
3454
3528
|
// Mutable ref so extracted helper functions can increment the shared counter
|