@izara_project/izara-core-generate-service-code 1.0.68 → 1.0.69
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/package.json +1 -1
- package/scripts/format-ejs.js +1 -2
- package/src/compile/compileProject.js +26 -10
- package/src/compile/loadSchemas.js +47 -23
- package/src/emit/crud/emitCrudApplicationArtifacts.js +188 -41
- package/src/emit/crud/emitDynamicCrudFlowSchemas.js +34 -17
- package/src/emit/crud/generateCrudApplicationArtifacts.js +14 -6
- package/src/emit/crud/generateDynamicCrudFlowSchemas.js +4 -1
- package/src/emit/crud/templates/GetEndpoint_Main.ejs +1 -1
- package/src/emit/custom/customFlows.js +23 -13
- package/src/emit/custom/emitCustomFlowApplicationArtifacts.js +515 -113
- package/src/emit/custom/generateCustomFlowApplicationArtifacts.js +28 -8
- package/src/emit/custom/templates/s3/FlowHandler_HdrS3.ejs +2 -2
- package/src/emit/custom/templates/s3/ReservedLimit_HdrSqs.ejs +1 -1
- package/src/emit/function/functionYamlArtifact.js +270 -97
- package/src/emit/function/templates/ApiFunction_Yaml.ejs +7 -0
- package/src/emit/function/templates/EventBridgeFunction_Yaml.ejs +6 -0
- package/src/emit/function/templates/InvFunction_Yaml.ejs +5 -0
- package/src/emit/function/templates/S3Function_Yaml.ejs +16 -0
- package/src/emit/function/templates/SqsFunction_Yaml.ejs +10 -0
- package/src/emit/function/templates/WbsFunction_Yaml.ejs +10 -0
- package/src/emit/rbac/emitDynamicRbacFlowSchemas.js +28 -30
- package/src/emit/rbac/emitRbacApplicationArtifacts.js +6 -6
- package/src/emit/rbac/generateCreateTargetRoleHandler.js +6 -4
- package/src/emit/rbac/generateDynamicRbacFlowSchemas.js +8 -2
- package/src/emit/rbac/rbacFlowTag.js +3 -1
- package/src/emit/rbac/templates/UserRbacFlow_Main.ejs +1 -1
- package/src/emit/relationship/emitDynamicRelationshipFlowSchemas.js +26 -10
- package/src/emit/relationship/emitRelationshipApplicationArtifacts.js +179 -37
- package/src/emit/relationship/generateDynamicRelationshipFlowSchemas.js +8 -2
- package/src/emit/relationship/generateRelationshipApplicationArtifacts.js +15 -6
- package/src/emit/resource/emitResourceArtifacts.js +187 -114
- package/src/emit/resource/generateResourceArtifacts.js +15 -6
- package/src/emit/resource/templates/DynamoDBTable_Yaml.ejs +18 -18
- package/src/emit/resource/templates/FlowOut_Yaml.ejs +5 -5
- package/src/emit/resource/templates/GeneratedLambdaRoleServiceConfig.ejs +10 -9
- package/src/emit/resource/templates/ResourceBeforeLogicalYaml.ejs +44 -44
- package/src/emit/resource/templates/ResourceSqsYaml.ejs +36 -36
- package/src/emit/resource/templates/ResourceYaml.ejs +48 -48
- package/src/emit/resource/templates/S3Subscription_Yaml.ejs +7 -0
- package/src/emit/resource/templates/SharedResource_Yaml.ejs +11 -11
- package/src/emit/resource/templates/SystemDynamoDB_Yaml.ejs +105 -105
- package/src/emit/resource/templates/WebSocketSubscription_Yaml.ejs +7 -0
- package/src/emit/system/emitSystemWebSocketArtifacts.js +61 -165
- package/src/emit/system/templates/WebSocketComplete_HdrSqs.ejs +32 -0
- package/src/emit/system/templates/WebSocketComplete_Main.ejs +46 -0
- package/src/emit/system/templates/WebSocketComplete_Yaml.ejs +19 -0
- package/src/emit/system/templates/WebSocketConnect.ejs +25 -0
- package/src/emit/system/templates/WebSocketConnect_Yaml.ejs +11 -0
- package/src/generate.js +136 -33
- package/src/generateIntTest.js +76 -28
- package/src/generateResourceIntTest.js +35 -12
- package/src/generateResources/IntTest/generateResources/generateResources.js +5 -2
- package/src/generateResources/IntTest/generateResources/templateResources.ejs +18 -30
- package/src/generateResources/IntTest/generateTests/events/events.js +1 -1
- package/src/generateResources/IntTest/generateTests/events/templateEvents.ejs +13 -10
- package/src/generateResources/IntTest/generateTests/generateTemplate.js +2 -2
- package/src/generateResources/IntTest/generateTests/pathIntTest/pathIntTest.js +2 -1
- package/src/generateResources/IntTest/generateTests/pathIntTest/template.ejs +1 -3
- package/src/generateResources/IntTest/generateTests/tests/templateTests.ejs +1 -1
- package/src/generateResources/IntTest/generateTests/tests/tests.js +7 -3
- package/src/generateResources/IntTest/libs/{libs.js → IntTestGenerateLib.js} +34 -23
- package/src/generateResources/IntTest/upload/uploadIntTest.js +33 -19
- package/src/intTestGeneratorShared.js +15 -10
- package/src/output/commitArtifacts.js +99 -39
- package/src/output/mergeHooks.js +24 -10
- package/src/output/validateArtifacts.js +37 -27
- package/src/plan/buildGenerationPlan.js +24 -13
- package/test/core.test.js +116 -49
- package/test/createTargetRoleHandler.test.js +1 -1
- package/test/crudApplicationArtifacts.test.js +128 -23
- package/test/customFlowApplicationArtifacts.test.js +353 -71
- package/test/dynamicCrudFlowSchemas.test.js +74 -26
- package/test/dynamicRbacFlowSchemas.test.js +61 -14
- package/test/dynamicRelationshipFlowSchemas.test.js +65 -18
- package/test/functionYamlArtifact.test.js +67 -10
- package/test/intTestApi.test.js +9 -10
- package/test/publicApi.test.js +176 -34
- package/test/relationshipApplicationArtifacts.test.js +90 -19
- package/test/resourceArtifacts.test.js +172 -32
- package/src/emit/relationship/dynamicRelationshipFlowSchemaTemplate.js +0 -11
|
@@ -22,7 +22,7 @@ const __dirname = path.dirname(__filename);
|
|
|
22
22
|
|
|
23
23
|
const templatePath = path.join(__dirname, './templateTests.ejs');
|
|
24
24
|
|
|
25
|
-
import libs from '../../libs/
|
|
25
|
+
import libs from '../../libs/IntTestGenerateLib.js';
|
|
26
26
|
const {
|
|
27
27
|
createTestFunctionName,
|
|
28
28
|
getHandlerFromEventTag,
|
|
@@ -31,7 +31,11 @@ const {
|
|
|
31
31
|
setInvokeTest
|
|
32
32
|
} = libs;
|
|
33
33
|
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
HANDLER,
|
|
36
|
+
GENERATE_SEED_DATA_TYPE,
|
|
37
|
+
SEED_DATA_TAG
|
|
38
|
+
} from '../../consts.js';
|
|
35
39
|
|
|
36
40
|
async function data(_izContext, allIntTestConfig, srcPath, serviceTag) {
|
|
37
41
|
let createSourceData = [];
|
|
@@ -139,7 +143,7 @@ async function data(_izContext, allIntTestConfig, srcPath, serviceTag) {
|
|
|
139
143
|
// baseIntTest.stage[currentStageIdx].invokes = invokeTest.invokeEventTags;
|
|
140
144
|
stages[currentStageIdx].invokes = invokeTest.invokeEventTags;
|
|
141
145
|
|
|
142
|
-
const invokeStagesCloned = invokeTest.invokeStages.map(stage =>
|
|
146
|
+
const invokeStagesCloned = invokeTest.invokeStages.map((stage) =>
|
|
143
147
|
structuredClone(stage)
|
|
144
148
|
);
|
|
145
149
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getObjectSchema,
|
|
3
|
+
consts as coreConsts
|
|
4
|
+
} from '@izara_project/izara-core-library-service-schemas';
|
|
2
5
|
const { getDataFromPath } = getObjectSchema;
|
|
3
6
|
const { HANDLER } = coreConsts;
|
|
4
|
-
import
|
|
7
|
+
import yamlLib from 'yaml';
|
|
5
8
|
import fs from 'fs';
|
|
6
9
|
import path from 'path';
|
|
7
10
|
|
|
@@ -22,13 +25,13 @@ function directoryPath(dirPath, result = []) {
|
|
|
22
25
|
directoryPath(fullPath, result); // ✅ ใช้ result เดียวกัน
|
|
23
26
|
} else if (stat.isFile()) {
|
|
24
27
|
if (
|
|
25
|
-
!fullPath.includes(
|
|
26
|
-
!fullPath.includes(
|
|
28
|
+
!fullPath.includes('shared-resource.yml') &&
|
|
29
|
+
!fullPath.includes('core-function.yml')
|
|
27
30
|
) {
|
|
28
|
-
if (fullPath.endsWith(
|
|
31
|
+
if (fullPath.endsWith('.yml')) {
|
|
29
32
|
result.push(fullPath);
|
|
30
33
|
} else {
|
|
31
|
-
console.log(
|
|
34
|
+
console.log('Skipped File:', fullPath);
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
}
|
|
@@ -37,19 +40,19 @@ function directoryPath(dirPath, result = []) {
|
|
|
37
40
|
let functionComponent = {};
|
|
38
41
|
|
|
39
42
|
for (let ymlFilesPath of result) {
|
|
40
|
-
const fileContent = fs.readFileSync(ymlFilesPath,
|
|
41
|
-
const parsedYaml =
|
|
43
|
+
const fileContent = fs.readFileSync(ymlFilesPath, 'utf8');
|
|
44
|
+
const parsedYaml = yamlLib.parse(fileContent);
|
|
42
45
|
|
|
43
46
|
for (const component of Object.values(parsedYaml)) {
|
|
44
47
|
if (component.handler && component.name) {
|
|
45
48
|
const clearName = component.name.replace(
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
'${self:custom.iz_resourcePrefix}',
|
|
50
|
+
''
|
|
48
51
|
);
|
|
49
|
-
const clearMain = component.handler.replace(/\.main$/,
|
|
52
|
+
const clearMain = component.handler.replace(/\.main$/, '');
|
|
50
53
|
|
|
51
54
|
Object.assign(functionComponent, {
|
|
52
|
-
[clearName]: clearMain
|
|
55
|
+
[clearName]: clearMain
|
|
53
56
|
});
|
|
54
57
|
}
|
|
55
58
|
}
|
|
@@ -347,16 +350,19 @@ function setNoInvokeTest(
|
|
|
347
350
|
// setTests
|
|
348
351
|
|
|
349
352
|
function createEventCaseByHandler(handler, messageAttributes = {}) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
? { messageAttributes: setEventMessageAttributes(messageAttributes) }
|
|
357
|
-
: {})
|
|
353
|
+
if (handler === HANDLER.hdrDsq) {
|
|
354
|
+
return { DirectSqs: true };
|
|
355
|
+
}
|
|
356
|
+
if (handler === HANDLER.hdrSqs) {
|
|
357
|
+
if (messageAttributes && Object.keys(messageAttributes).length > 0) {
|
|
358
|
+
return { messageAttributes: setEventMessageAttributes(messageAttributes) };
|
|
358
359
|
}
|
|
359
|
-
|
|
360
|
+
return {};
|
|
361
|
+
}
|
|
362
|
+
if (handler === HANDLER.hdrApi || handler === HANDLER.hdrInv) {
|
|
363
|
+
return {};
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
360
366
|
}
|
|
361
367
|
|
|
362
368
|
function getHandlerFromEventTag(testTag) {
|
|
@@ -395,11 +401,16 @@ function createResourceName(eventTag) {
|
|
|
395
401
|
async function getIntTestConfig(_izContext, intTestPath, rootPath) {
|
|
396
402
|
// ponytail: find first existing integration test libs folder or fall back to standard path
|
|
397
403
|
const targetLibsPath = rootPath.includes('generateIntegrationTest')
|
|
398
|
-
? path.join(
|
|
404
|
+
? path.join(
|
|
405
|
+
rootPath.split('generateIntegrationTest')[0],
|
|
406
|
+
'generateIntegrationTest',
|
|
407
|
+
'libs'
|
|
408
|
+
)
|
|
399
409
|
: [
|
|
400
410
|
path.join(rootPath, 'app/src/generateIntegrationTest/libs'),
|
|
401
411
|
path.join(rootPath, 'src/generateIntegrationTest/libs')
|
|
402
|
-
].find(fs.existsSync) ||
|
|
412
|
+
].find(fs.existsSync) ||
|
|
413
|
+
path.join(rootPath, 'src/generateIntegrationTest/libs');
|
|
403
414
|
|
|
404
415
|
return await getObjectSchema.getDataFromPath(
|
|
405
416
|
_izContext,
|
|
@@ -22,15 +22,17 @@ import Logger from '@izara_project/izara-core-library-logger';
|
|
|
22
22
|
const DEFAULT_BUCKET = 'integrationtest-config-us-east-2';
|
|
23
23
|
const _izContext = { Logger };
|
|
24
24
|
|
|
25
|
-
function
|
|
26
|
-
if (!fs.existsSync(dirPath))
|
|
25
|
+
function _getAllFiles(dirPath, filePaths = []) {
|
|
26
|
+
if (!fs.existsSync(dirPath)) {
|
|
27
|
+
return filePaths;
|
|
28
|
+
}
|
|
27
29
|
|
|
28
30
|
for (const file of fs.readdirSync(dirPath)) {
|
|
29
31
|
const fullPath = path.join(dirPath, file);
|
|
30
32
|
const stat = fs.statSync(fullPath);
|
|
31
33
|
|
|
32
34
|
if (stat.isDirectory()) {
|
|
33
|
-
|
|
35
|
+
_getAllFiles(fullPath, filePaths);
|
|
34
36
|
continue;
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -42,15 +44,18 @@ function getAllFiles(dirPath, filePaths = []) {
|
|
|
42
44
|
|
|
43
45
|
async function bucketPathForUpload(folderPath, settings = {}) {
|
|
44
46
|
const dataToUploads = [];
|
|
45
|
-
const files =
|
|
47
|
+
const files = _getAllFiles(folderPath);
|
|
46
48
|
|
|
47
49
|
for (const filePath of files) {
|
|
48
50
|
const fileContent = fs.readFileSync(filePath);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
let index = -1;
|
|
52
|
+
if (settings.serviceTag) {
|
|
53
|
+
index = filePath.indexOf(settings.serviceTag);
|
|
54
|
+
}
|
|
55
|
+
let relativePath = path.basename(filePath);
|
|
56
|
+
if (index !== -1) {
|
|
57
|
+
relativePath = filePath.substring(index);
|
|
58
|
+
}
|
|
54
59
|
const s3Key = path
|
|
55
60
|
.join('test_config', settings.s3Prefix || '', relativePath)
|
|
56
61
|
.split(path.sep)
|
|
@@ -64,18 +69,27 @@ async function bucketPathForUpload(folderPath, settings = {}) {
|
|
|
64
69
|
|
|
65
70
|
async function uploadToS3(folderPath, settings = {}) {
|
|
66
71
|
const uploads = await bucketPathForUpload(folderPath, settings);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
let bucket = DEFAULT_BUCKET;
|
|
73
|
+
if (settings.bucket) {
|
|
74
|
+
bucket = settings.bucket;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await Promise.all(
|
|
78
|
+
uploads.map(async ({ key, fileContent }) => {
|
|
79
|
+
await s3.putObjectS3(_izContext, {
|
|
80
|
+
Bucket: bucket,
|
|
81
|
+
Key: key,
|
|
82
|
+
Body: fileContent,
|
|
83
|
+
ContentType: 'application/json'
|
|
84
|
+
});
|
|
75
85
|
})
|
|
76
|
-
)
|
|
86
|
+
);
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
const keys = [];
|
|
89
|
+
for (const { key } of uploads) {
|
|
90
|
+
keys.push(key);
|
|
91
|
+
}
|
|
92
|
+
return keys;
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
export { DEFAULT_BUCKET, bucketPathForUpload, uploadToS3 };
|
|
@@ -1,32 +1,37 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, rmSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import
|
|
3
|
+
import yamlLib from 'yaml';
|
|
4
4
|
|
|
5
5
|
function getServiceTag(rootPath) {
|
|
6
6
|
const configPath = join(rootPath, 'config', 'serverless.config.yml');
|
|
7
|
-
const config =
|
|
7
|
+
const config = yamlLib.parse(readFileSync(configPath, 'utf8'));
|
|
8
8
|
return config?.main_config?.iz_serviceTag;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function getIntTestOutputPath(outputPath, projectRootPath = outputPath) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
if (existsSync(join(projectRootPath, 'app'))) {
|
|
13
|
+
return join(outputPath, 'app', 'src', 'generateIntegrationTest');
|
|
14
|
+
}
|
|
15
|
+
return join(outputPath, 'src', 'generateIntegrationTest');
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
function
|
|
18
|
-
if (!savePath || !existsSync(savePath))
|
|
18
|
+
function _clearSavePath(savePath, preserveFileNames = []) {
|
|
19
|
+
if (!savePath || !existsSync(savePath)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
19
22
|
|
|
20
23
|
const preserved = new Set(preserveFileNames);
|
|
21
24
|
for (const entry of readdirSync(savePath)) {
|
|
22
|
-
if (preserved.has(entry))
|
|
25
|
+
if (preserved.has(entry)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
23
28
|
rmSync(join(savePath, entry), { recursive: true, force: true });
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
async function runTemplateItems(items, { writeSource, errorPrefix }) {
|
|
28
33
|
const normalizedItems = (items || []).filter(
|
|
29
|
-
item => !(Array.isArray(item) && item.length === 0)
|
|
34
|
+
(item) => !(Array.isArray(item) && item.length === 0)
|
|
30
35
|
);
|
|
31
36
|
if (!normalizedItems.length) {
|
|
32
37
|
return;
|
|
@@ -55,7 +60,7 @@ async function runTemplateItems(items, { writeSource, errorPrefix }) {
|
|
|
55
60
|
const savePath = item.setting?.savePath;
|
|
56
61
|
const cleanSavePathEnabled = item.setting?.cleanSavePath !== false;
|
|
57
62
|
if (savePath && cleanSavePathEnabled && !clearedPaths.has(savePath)) {
|
|
58
|
-
|
|
63
|
+
_clearSavePath(savePath, item.setting?.preserveFileNames || []);
|
|
59
64
|
clearedPaths.add(savePath);
|
|
60
65
|
}
|
|
61
66
|
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
copyFile,
|
|
4
|
+
lstat,
|
|
5
|
+
mkdir,
|
|
6
|
+
mkdtemp,
|
|
7
|
+
readFile,
|
|
8
|
+
realpath,
|
|
9
|
+
rename,
|
|
10
|
+
rm,
|
|
11
|
+
unlink,
|
|
12
|
+
writeFile
|
|
13
|
+
} from 'node:fs/promises';
|
|
3
14
|
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
4
15
|
|
|
5
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
normalizeArtifactPaths,
|
|
18
|
+
validateArtifacts
|
|
19
|
+
} from './validateArtifacts.js';
|
|
6
20
|
import { mergeHooks } from './mergeHooks.js';
|
|
7
21
|
|
|
8
22
|
const MANAGED_ARTIFACTS_PATH = '.generate-service-code-v2-managed.json';
|
|
@@ -10,36 +24,51 @@ const STAGING_PREFIX = '.generate-service-code-v2-stage-';
|
|
|
10
24
|
|
|
11
25
|
export async function commitArtifacts(plan) {
|
|
12
26
|
if (!plan?.project?.outputPath || !Array.isArray(plan.artifacts)) {
|
|
13
|
-
throw new TypeError(
|
|
27
|
+
throw new TypeError(
|
|
28
|
+
'GenerationPlan must include project.outputPath and artifacts'
|
|
29
|
+
);
|
|
14
30
|
}
|
|
15
31
|
|
|
16
32
|
const outputRoot = resolve(plan.project.outputPath);
|
|
17
33
|
const artifacts = validateArtifacts(plan.artifacts);
|
|
18
34
|
await mkdir(outputRoot, { recursive: true });
|
|
19
35
|
const physicalOutputRoot = await realpath(outputRoot);
|
|
20
|
-
const manifestPath = await
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
const manifestPath = await _resolveSafeTarget(
|
|
37
|
+
physicalOutputRoot,
|
|
38
|
+
MANAGED_ARTIFACTS_PATH
|
|
39
|
+
);
|
|
40
|
+
const managedArtifacts = await _loadManagedArtifacts(manifestPath);
|
|
41
|
+
const managedScopes = _normalizeManagedScopes(plan);
|
|
42
|
+
const stalePaths = _staleManagedPaths(
|
|
43
|
+
managedScopes,
|
|
44
|
+
managedArtifacts,
|
|
45
|
+
artifacts
|
|
46
|
+
);
|
|
24
47
|
const mergedArtifacts = await mergeHooks(physicalOutputRoot, artifacts);
|
|
25
48
|
const stagedRoot = await mkdtemp(join(physicalOutputRoot, STAGING_PREFIX));
|
|
26
49
|
|
|
27
50
|
try {
|
|
28
|
-
|
|
51
|
+
let manifestContent;
|
|
52
|
+
if (managedScopes) {
|
|
53
|
+
manifestContent = _managedManifestContent(
|
|
54
|
+
managedScopes,
|
|
55
|
+
managedArtifacts,
|
|
56
|
+
mergedArtifacts
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const changes = await _stageChanges({
|
|
29
60
|
outputRoot: physicalOutputRoot,
|
|
30
61
|
stagedRoot,
|
|
31
62
|
artifacts: mergedArtifacts,
|
|
32
63
|
stalePaths,
|
|
33
64
|
manifestPath,
|
|
34
|
-
manifestContent
|
|
35
|
-
? managedManifestContent(managedScopes, managedArtifacts, mergedArtifacts)
|
|
36
|
-
: undefined
|
|
65
|
+
manifestContent
|
|
37
66
|
});
|
|
38
|
-
const snapshots = await
|
|
67
|
+
const snapshots = await _snapshotChanges(changes, stagedRoot);
|
|
39
68
|
try {
|
|
40
|
-
await
|
|
69
|
+
await _applyChanges(changes);
|
|
41
70
|
} catch (error) {
|
|
42
|
-
await
|
|
71
|
+
await _rollbackChanges(changes, snapshots);
|
|
43
72
|
throw error;
|
|
44
73
|
}
|
|
45
74
|
} finally {
|
|
@@ -49,21 +78,23 @@ export async function commitArtifacts(plan) {
|
|
|
49
78
|
return mergedArtifacts.map(({ path }) => ({ path }));
|
|
50
79
|
}
|
|
51
80
|
|
|
52
|
-
function
|
|
53
|
-
if (plan.managedScopes !== undefined) {
|
|
81
|
+
function _normalizeManagedScopes(plan) {
|
|
82
|
+
if (plan.managedScopes !== undefined && plan.managedScopes !== null) {
|
|
54
83
|
return plan.managedScopes;
|
|
55
84
|
}
|
|
56
|
-
if (plan.managedScope === undefined) {
|
|
57
|
-
return
|
|
85
|
+
if (plan.managedScope === undefined || plan.managedScope === null) {
|
|
86
|
+
return null;
|
|
58
87
|
}
|
|
59
|
-
return {
|
|
88
|
+
return {
|
|
89
|
+
[plan.managedScope]: normalizeArtifactPaths(plan.managedPaths ?? [])
|
|
90
|
+
};
|
|
60
91
|
}
|
|
61
92
|
|
|
62
|
-
function
|
|
93
|
+
function _staleManagedPaths(managedScopes, managedArtifacts, artifacts) {
|
|
63
94
|
if (!managedScopes) {
|
|
64
95
|
return [];
|
|
65
96
|
}
|
|
66
|
-
const emittedPaths = new Set(artifacts.map(artifact => artifact.path));
|
|
97
|
+
const emittedPaths = new Set(artifacts.map((artifact) => artifact.path));
|
|
67
98
|
const stalePaths = new Set();
|
|
68
99
|
for (const [scope, declaredPaths] of Object.entries(managedScopes)) {
|
|
69
100
|
if (!Object.hasOwn(managedArtifacts, scope)) {
|
|
@@ -78,35 +109,58 @@ function staleManagedPaths(managedScopes, managedArtifacts, artifacts) {
|
|
|
78
109
|
return [...stalePaths];
|
|
79
110
|
}
|
|
80
111
|
|
|
81
|
-
function
|
|
82
|
-
const emittedPaths = new Set(artifacts.map(artifact => artifact.path));
|
|
112
|
+
function _managedManifestContent(managedScopes, managedArtifacts, artifacts) {
|
|
113
|
+
const emittedPaths = new Set(artifacts.map((artifact) => artifact.path));
|
|
83
114
|
const nextManifest = { ...managedArtifacts };
|
|
84
115
|
for (const [scope, paths] of Object.entries(managedScopes)) {
|
|
85
|
-
nextManifest[scope] = paths.filter(path => emittedPaths.has(path));
|
|
116
|
+
nextManifest[scope] = paths.filter((path) => emittedPaths.has(path));
|
|
86
117
|
}
|
|
87
118
|
return `${JSON.stringify(nextManifest, null, 2)}\n`;
|
|
88
119
|
}
|
|
89
120
|
|
|
90
|
-
async function
|
|
121
|
+
async function _stageChanges({
|
|
122
|
+
outputRoot,
|
|
123
|
+
stagedRoot,
|
|
124
|
+
artifacts,
|
|
125
|
+
stalePaths,
|
|
126
|
+
manifestPath,
|
|
127
|
+
manifestContent
|
|
128
|
+
}) {
|
|
91
129
|
const changes = [];
|
|
92
130
|
for (const artifact of artifacts) {
|
|
93
|
-
const targetPath = await
|
|
94
|
-
const stagedPath = await
|
|
95
|
-
|
|
131
|
+
const targetPath = await _resolveSafeTarget(outputRoot, artifact.path);
|
|
132
|
+
const stagedPath = await _resolveSafeTarget(stagedRoot, artifact.path, {
|
|
133
|
+
createDirectories: true
|
|
134
|
+
});
|
|
135
|
+
await writeFile(stagedPath, artifact.content, {
|
|
136
|
+
encoding: 'utf8',
|
|
137
|
+
flag:
|
|
138
|
+
constants.O_WRONLY |
|
|
139
|
+
constants.O_CREAT |
|
|
140
|
+
constants.O_TRUNC |
|
|
141
|
+
constants.O_NOFOLLOW
|
|
142
|
+
});
|
|
96
143
|
changes.push({ targetPath, stagedPath });
|
|
97
144
|
}
|
|
98
145
|
for (const path of stalePaths) {
|
|
99
|
-
changes.push({
|
|
146
|
+
changes.push({
|
|
147
|
+
targetPath: await _resolveSafeTarget(outputRoot, path),
|
|
148
|
+
delete: true
|
|
149
|
+
});
|
|
100
150
|
}
|
|
101
151
|
if (manifestContent !== undefined) {
|
|
102
|
-
const stagedPath = await
|
|
152
|
+
const stagedPath = await _resolveSafeTarget(
|
|
153
|
+
stagedRoot,
|
|
154
|
+
MANAGED_ARTIFACTS_PATH,
|
|
155
|
+
{ createDirectories: true }
|
|
156
|
+
);
|
|
103
157
|
await writeFile(stagedPath, manifestContent, 'utf8');
|
|
104
158
|
changes.push({ targetPath: manifestPath, stagedPath });
|
|
105
159
|
}
|
|
106
160
|
return changes;
|
|
107
161
|
}
|
|
108
162
|
|
|
109
|
-
async function
|
|
163
|
+
async function _snapshotChanges(changes, stagedRoot) {
|
|
110
164
|
const snapshots = new Map();
|
|
111
165
|
for (const [index, change] of changes.entries()) {
|
|
112
166
|
try {
|
|
@@ -128,10 +182,10 @@ async function snapshotChanges(changes, stagedRoot) {
|
|
|
128
182
|
return snapshots;
|
|
129
183
|
}
|
|
130
184
|
|
|
131
|
-
async function
|
|
185
|
+
async function _applyChanges(changes) {
|
|
132
186
|
for (const change of changes) {
|
|
133
187
|
if (change.delete) {
|
|
134
|
-
await unlink(change.targetPath).catch(
|
|
188
|
+
await unlink(change.targetPath).catch(_ignoreMissingFile);
|
|
135
189
|
continue;
|
|
136
190
|
}
|
|
137
191
|
await mkdir(dirname(change.targetPath), { recursive: true });
|
|
@@ -139,11 +193,11 @@ async function applyChanges(changes) {
|
|
|
139
193
|
}
|
|
140
194
|
}
|
|
141
195
|
|
|
142
|
-
async function
|
|
196
|
+
async function _rollbackChanges(changes, snapshots) {
|
|
143
197
|
for (const change of [...changes].reverse()) {
|
|
144
198
|
const snapshot = snapshots.get(change.targetPath);
|
|
145
199
|
if (!snapshot) {
|
|
146
|
-
await unlink(change.targetPath).catch(
|
|
200
|
+
await unlink(change.targetPath).catch(_ignoreMissingFile);
|
|
147
201
|
continue;
|
|
148
202
|
}
|
|
149
203
|
await mkdir(dirname(change.targetPath), { recursive: true });
|
|
@@ -151,13 +205,13 @@ async function rollbackChanges(changes, snapshots) {
|
|
|
151
205
|
}
|
|
152
206
|
}
|
|
153
207
|
|
|
154
|
-
function
|
|
208
|
+
function _ignoreMissingFile(error) {
|
|
155
209
|
if (error.code !== 'ENOENT') {
|
|
156
210
|
throw error;
|
|
157
211
|
}
|
|
158
212
|
}
|
|
159
213
|
|
|
160
|
-
async function
|
|
214
|
+
async function _loadManagedArtifacts(manifestPath) {
|
|
161
215
|
try {
|
|
162
216
|
const parsed = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
163
217
|
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
|
|
@@ -175,7 +229,11 @@ async function loadManagedArtifacts(manifestPath) {
|
|
|
175
229
|
}
|
|
176
230
|
}
|
|
177
231
|
|
|
178
|
-
async function
|
|
232
|
+
async function _resolveSafeTarget(
|
|
233
|
+
outputRoot,
|
|
234
|
+
artifactPath,
|
|
235
|
+
{ createDirectories = false } = {}
|
|
236
|
+
) {
|
|
179
237
|
const targetPath = resolve(outputRoot, artifactPath);
|
|
180
238
|
if (relative(outputRoot, targetPath).startsWith('..')) {
|
|
181
239
|
throw new Error(`Artifact path escapes output root: ${artifactPath}`);
|
|
@@ -187,7 +245,9 @@ async function resolveSafeTarget(outputRoot, artifactPath, { createDirectories =
|
|
|
187
245
|
try {
|
|
188
246
|
const details = await lstat(currentPath);
|
|
189
247
|
if (details.isSymbolicLink() || !details.isDirectory()) {
|
|
190
|
-
throw new Error(
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Artifact path traverses a non-directory: ${artifactPath}`
|
|
250
|
+
);
|
|
191
251
|
}
|
|
192
252
|
} catch (error) {
|
|
193
253
|
if (error.code !== 'ENOENT') {
|
package/src/output/mergeHooks.js
CHANGED
|
@@ -2,10 +2,12 @@ import { lstat, readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { join, sep } from 'node:path';
|
|
3
3
|
|
|
4
4
|
export async function mergeHooks(outputRoot, artifacts) {
|
|
5
|
-
return Promise.all(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
return Promise.all(
|
|
6
|
+
artifacts.map(async (artifact) => ({
|
|
7
|
+
...artifact,
|
|
8
|
+
content: await mergeHook(outputRoot, artifact)
|
|
9
|
+
}))
|
|
10
|
+
);
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
async function mergeHook(outputRoot, artifact) {
|
|
@@ -15,7 +17,12 @@ async function mergeHook(outputRoot, artifact) {
|
|
|
15
17
|
return artifact.content;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
const hookPath = join(
|
|
20
|
+
const hookPath = join(
|
|
21
|
+
outputRoot,
|
|
22
|
+
...parts.slice(0, sourceIndex),
|
|
23
|
+
'hook',
|
|
24
|
+
...parts.slice(sourceIndex + 1)
|
|
25
|
+
);
|
|
19
26
|
try {
|
|
20
27
|
if (!(await lstat(hookPath)).isFile()) {
|
|
21
28
|
return artifact.content;
|
|
@@ -33,22 +40,29 @@ async function mergeHook(outputRoot, artifact) {
|
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
const hookContent = await readFile(hookPath, 'utf8');
|
|
36
|
-
return artifact.content.replace(
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
return artifact.content.replace(
|
|
44
|
+
matchers.anyTag,
|
|
45
|
+
(match, tagName) =>
|
|
46
|
+
hookContent.match(matchers.tagPattern(tagName))?.[0] ?? match
|
|
47
|
+
);
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
function hookMatchers(filePath) {
|
|
42
51
|
if (filePath.endsWith('.js')) {
|
|
43
52
|
return {
|
|
44
53
|
anyTag: /\/\/\(<(\w+)>\)([\s\S]*?)\/\/\(<\/\1>\)/gm,
|
|
45
|
-
tagPattern:
|
|
54
|
+
tagPattern: (name) =>
|
|
55
|
+
new RegExp(
|
|
56
|
+
`\\/\\/\\(<${name}>\\)([\\s\\S]*?)\\/\\/\\(<\\/${name}>\\)`,
|
|
57
|
+
'gm'
|
|
58
|
+
)
|
|
46
59
|
};
|
|
47
60
|
}
|
|
48
61
|
if (filePath.endsWith('.yml') || filePath.endsWith('.yaml')) {
|
|
49
62
|
return {
|
|
50
63
|
anyTag: /#<#(\w+)#>([\s\S]*?)#<#\/\1#>/gm,
|
|
51
|
-
tagPattern:
|
|
64
|
+
tagPattern: (name) =>
|
|
65
|
+
new RegExp(`#<#${name}#>([\\s\\S]*?)#<#\\/${name}#>`, 'gm')
|
|
52
66
|
};
|
|
53
67
|
}
|
|
54
68
|
return null;
|
|
@@ -6,24 +6,32 @@ export function normalizeArtifacts(artifacts) {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const paths = new Set();
|
|
9
|
-
return Object.freeze(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
9
|
+
return Object.freeze(
|
|
10
|
+
artifacts.map((artifact) => {
|
|
11
|
+
if (
|
|
12
|
+
!artifact ||
|
|
13
|
+
typeof artifact.path !== 'string' ||
|
|
14
|
+
artifact.path.trim() === ''
|
|
15
|
+
) {
|
|
16
|
+
throw new TypeError('Artifact.path must be a non-empty relative path');
|
|
17
|
+
}
|
|
18
|
+
if (isAbsolute(artifact.path)) {
|
|
19
|
+
throw new TypeError(`Artifact.path must be relative: ${artifact.path}`);
|
|
20
|
+
}
|
|
21
|
+
if (typeof artifact.content !== 'string') {
|
|
22
|
+
throw new TypeError(
|
|
23
|
+
`Artifact.content must be a string: ${artifact.path}`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const path = _normalizeArtifactPath(artifact.path);
|
|
28
|
+
if (paths.has(path)) {
|
|
29
|
+
throw new Error(`Duplicate artifact path: ${path}`);
|
|
30
|
+
}
|
|
31
|
+
paths.add(path);
|
|
32
|
+
return Object.freeze({ path, content: artifact.content });
|
|
33
|
+
})
|
|
34
|
+
);
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
export function validateArtifacts(artifacts) {
|
|
@@ -36,17 +44,19 @@ export function normalizeArtifactPaths(paths) {
|
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
const seenPaths = new Set();
|
|
39
|
-
return Object.freeze(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
return Object.freeze(
|
|
48
|
+
paths.map((path) => {
|
|
49
|
+
const normalizedPath = _normalizeArtifactPath(path);
|
|
50
|
+
if (seenPaths.has(normalizedPath)) {
|
|
51
|
+
throw new Error(`Duplicate artifact path: ${normalizedPath}`);
|
|
52
|
+
}
|
|
53
|
+
seenPaths.add(normalizedPath);
|
|
54
|
+
return normalizedPath;
|
|
55
|
+
})
|
|
56
|
+
);
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
function
|
|
59
|
+
function _normalizeArtifactPath(path) {
|
|
50
60
|
if (typeof path !== 'string' || path.trim() === '') {
|
|
51
61
|
throw new TypeError('Artifact.path must be a non-empty relative path');
|
|
52
62
|
}
|