Skip to content

Commit

Permalink
feat: Additional files for transform (#278)
Browse files Browse the repository at this point in the history
  • Loading branch information
doosuu authored Jun 11, 2024
1 parent df566c0 commit d810e8c
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 51 deletions.
177 changes: 135 additions & 42 deletions src/modules/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,70 +13,167 @@
// SPDX-License-Identifier: Apache-2.0

import { Stats } from 'node:fs';
import { extname, join } from 'node:path';
import { basename, extname, join } from 'node:path';
import { cwd } from 'node:process';
import { Transform, TransformCallback, TransformOptions } from 'node:stream';
import copy from 'recursive-copy';
import { CliFileSystem } from '../utils/fs-bridge';
import { ComponentContext, ComponentManifest } from './component';
import { PackageConfig } from './package';
import { ComponentContext } from './component';
import { VariableCollection } from './variables';

const SUPPORTED_TEXT_FILES_ARRAY = ['.md', '.yaml', '.yml', '.txt', '.json', '.sh', '.html', '.htm', '.xml', '.tpl'];
const NOTICE_COMMENT = 'This file is maintained by velocitas CLI, do not modify manually. Change settings in .velocitas.json';

class ReplaceVariablesStream extends Transform {
/**
* Interface for implementing comment insertion hints for supported text files.
*/
interface CommentInsertionHint {
// one or multiple extensions of the file
ext?: string[] | string;

// one or more special filenames of the file (e.g. Dockerfile)
filename?: string[] | string;

// a comment template which will be used for the comment line. Occurrences of %COMMENT% will be replaced by the actual comment literal.
commentTemplate?: string;

// a matcher which finds an appropriate position in the text to insert the comment **after**
insertAfterLineMatcher?: string | RegExp;
}

const filetypesForCommentInsertion: CommentInsertionHint[] = [
{
ext: '.txt',
},
{
ext: ['.md'],
commentTemplate: '<!-- %COMMENT% -->',
},
{
ext: ['.html', '.htm', '.xml', '.tpl'],
commentTemplate: '<!-- %COMMENT% -->',
insertAfterLineMatcher: new RegExp(`\\<\\?xml\\s.*?\\s\\?\\>`),
},
{
ext: '.json',
commentTemplate: '// %COMMENT%',
},
{
ext: ['.yml', '.yaml'],
commentTemplate: '# %COMMENT%',
},
{
ext: '.sh',
commentTemplate: '# %COMMENT%',
insertAfterLineMatcher: '#!/bin/bash',
},
{
ext: '.dockerfile',
filename: 'Dockerfile',
commentTemplate: '# %COMMENT%',
},
];

function maybeCreateReplaceVariablesTransform(filename: string, variables: VariableCollection): ReplaceVariablesTransform | null {
const transform = new ReplaceVariablesTransform(filename, variables);
if (transform.canHandleFile()) {
return transform;
}
return null;
}

class ReplaceVariablesTransform extends Transform {
private _filename: string;
private _fileExt: string;
private _variables: VariableCollection;
private _firstChunk: boolean;

constructor(fileExt: string, variables: VariableCollection, opts?: TransformOptions | undefined) {
constructor(filename: string, variables: VariableCollection, opts?: TransformOptions | undefined) {
super({ ...opts, readableObjectMode: true, writableObjectMode: true });
this._fileExt = fileExt;
this._filename = filename;
this._fileExt = extname(this._filename);
this._variables = variables;
this._firstChunk = true;
}

private _hasMatchingFileExtension(transformableFiletype: CommentInsertionHint): boolean {
const ext = transformableFiletype.ext;
return (typeof ext === 'string' && ext === this._fileExt) || (Array.isArray(ext) && ext.includes(this._fileExt));
}

private _hasMatchingFilename(transformableFiletype: CommentInsertionHint): boolean {
const filename = transformableFiletype.filename;
return (
(typeof filename === 'string' && filename === this._filename) || (Array.isArray(filename) && filename.includes(this._filename))
);
}

private _isKnownFile(transformableFiletype: CommentInsertionHint): boolean {
return this._hasMatchingFileExtension(transformableFiletype) || this._hasMatchingFilename(transformableFiletype);
}

private _findInsertionLine(textChunk: string, transformableFiletype: CommentInsertionHint): string | undefined {
const needle = transformableFiletype.insertAfterLineMatcher;
if (!needle) {
return undefined;
}

let insertionLine: string | undefined;
if (typeof needle === 'string') {
insertionLine = textChunk.split('\n').find((line) => line === needle);
} else if (needle instanceof RegExp) {
insertionLine = needle.exec(textChunk)?.[0];
}
return insertionLine;
}

private _insertCommentAfterLine(textChunk: string, startingLine: string, noticeComment: string): string {
return `${textChunk.slice(0, startingLine.length)}\n${noticeComment}${textChunk.slice(startingLine.length)}`;
}

private _tryInsertNoticeComment(textChunk: string): string {
for (const transformableFiletype of filetypesForCommentInsertion) {
if (!transformableFiletype.commentTemplate) {
continue;
}

if (!this._isKnownFile(transformableFiletype)) {
continue;
}

const comment = transformableFiletype.commentTemplate?.replace('%COMMENT%', NOTICE_COMMENT);
const insertionLine = this._findInsertionLine(textChunk, transformableFiletype);
if (insertionLine) {
textChunk = this._insertCommentAfterLine(textChunk, insertionLine, comment);
} else {
// no line found to insert after, hence insert at the very top
textChunk = `${comment}\n${textChunk}`;
}
}

return textChunk;
}

// we are overwriting the method from transform, hence we need to disable the name warning
// eslint-disable-next-line @typescript-eslint/naming-convention
_transform(chunk: any, _: string, callback: TransformCallback) {
let result = this._variables.substitute(chunk.toString());
let noticeComment: string;
const notice = 'This file is maintained by velocitas CLI, do not modify manually. Change settings in .velocitas.json';
const shebang = '#!/bin/bash';
const xmlDeclarationRegExp = new RegExp(`\\<\\?xml\\s.*?\\s\\?\\>`);
let textChunk = this._variables.substitute(chunk.toString());

if (this._firstChunk) {
if (['.txt'].includes(this._fileExt)) {
result = `${notice}\n${result}`;
} else if (['.md', '.html', '.htm', '.xml', '.tpl'].includes(this._fileExt)) {
noticeComment = `<!-- ${notice} -->`;
const xmlDeclarationArray = xmlDeclarationRegExp.exec(result);
if (xmlDeclarationArray !== null && result.startsWith(xmlDeclarationArray[0])) {
result = this._injectNoticeAfterStartLine(result, xmlDeclarationArray[0], noticeComment);
} else {
result = `${noticeComment}\n${result}`;
}
} else if (['.yaml', '.yml', '.sh'].includes(this._fileExt)) {
noticeComment = `# ${notice}`;
if (result.startsWith(shebang)) {
result = this._injectNoticeAfterStartLine(result, shebang, noticeComment);
} else {
result = `${noticeComment}\n${result}`;
}
} else if (['.json'].includes(this._fileExt)) {
noticeComment = `// ${notice}`;
result = `${noticeComment}\n${result}`;
}

textChunk = this._tryInsertNoticeComment(textChunk);
this._firstChunk = false;
}

this.push(result);
this.push(textChunk);
callback();
}

private _injectNoticeAfterStartLine(result: string, startingLine: string, noticeComment: string) {
return `${result.slice(0, startingLine.length)}\n${noticeComment}${result.slice(startingLine.length)}`;
public canHandleFile(): boolean {
for (const transformableFiletype of filetypesForCommentInsertion) {
if (this._isKnownFile(transformableFiletype)) {
return true;
}
}
return false;
}
}

Expand All @@ -96,11 +193,7 @@ export function installComponent(component: ComponentContext, variables: Variabl
dot: true,
overwrite: true,
transform: function (src: string, _: string, stats: Stats) {
if (!SUPPORTED_TEXT_FILES_ARRAY.includes(extname(src))) {
return null;
}

return new ReplaceVariablesStream(extname(src), variables);
return maybeCreateReplaceVariablesTransform(basename(src), variables);
},
});
}
Expand Down
80 changes: 72 additions & 8 deletions test/system-test/sync.stest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const packageManifestTwo = JSON.parse(
const fileOneDestination = packageManifestOne.components[0].files[0].dst;
const fileTwoDestination = packageManifestTwo.components[0].files[0].dst;
const fileThreeDestination = packageManifestTwo.components[1].files[0].dst;
const fileFourDestination = packageManifestTwo.components[2].files[0].dst;

describe('CLI command', () => {
describe('sync', () => {
Expand All @@ -45,24 +46,87 @@ describe('CLI command', () => {
const syncOutput = spawnSync(VELOCITAS_PROCESS, ['sync'], { encoding: DEFAULT_BUFFER_ENCODING });
expect(syncOutput.status).to.equal(0);

const resultOne = spawnSync(`./${fileOneDestination}`, {
const resultOne = readFileSync(`./${fileOneDestination}`, {
encoding: DEFAULT_BUFFER_ENCODING,
});
expect(resultOne.stdout).to.contain('projectTest');
expect(resultOne.stdout).to.contain('packageTestOne');
expect(resultOne.stdout).to.contain(1);
expect(resultOne).to.equal(
`#!/bin/bash
# This file is maintained by velocitas CLI, do not modify manually. Change settings in .velocitas.json
# Copyright (c) 2024 Contributors to the Eclipse Foundation
#
# This program and the accompanying materials are made available under the
# terms of the Apache License, Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# SPDX-License-Identifier: Apache-2.0
const resultTwo = spawnSync(`./${fileTwoDestination}`, {
echo projectTest
echo packageTestOne
echo 1
`,
);

const resultTwo = readFileSync(`./${fileTwoDestination}`, {
encoding: DEFAULT_BUFFER_ENCODING,
});
expect(resultTwo.stdout).to.contain('projectTest');
expect(resultTwo.stdout).to.contain('packageTestTwo');
expect(resultTwo.stdout).to.contain(2);
expect(resultTwo).to.equal(
`#!/bin/bash
# This file is maintained by velocitas CLI, do not modify manually. Change settings in .velocitas.json
# Copyright (c) 2024 Contributors to the Eclipse Foundation
#
# This program and the accompanying materials are made available under the
# terms of the Apache License, Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# SPDX-License-Identifier: Apache-2.0
echo projectTest
echo packageTestTwo
echo 2
`,
);

const resultThree = readFileSync(`./${fileThreeDestination}`, {
encoding: DEFAULT_BUFFER_ENCODING,
});
expect(resultThree).to.equal('A nested file\n');

const resultFour = readFileSync(`./${fileFourDestination}`, {
encoding: DEFAULT_BUFFER_ENCODING,
});
expect(resultFour).to.equal(
`# This file is maintained by velocitas CLI, do not modify manually. Change settings in .velocitas.json
# Copyright (c) 2024 Contributors to the Eclipse Foundation
#
# This program and the accompanying materials are made available under the
# terms of the Apache License, Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# SPDX-License-Identifier: Apache-2.0
FROM packageTestTwo
RUN ls -al
`,
);
});
});
});
3 changes: 2 additions & 1 deletion testbench/test-sync/.velocitas.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"components": [
"test-componentOne",
"test-componentTwo",
"test-componentThree"
"test-componentThree",
"test-componentFour"
],
"variables": {
"projectVariable": "projectTest",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) 2024 Contributors to the Eclipse Foundation
#
# This program and the accompanying materials are made available under the
# terms of the Apache License, Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# SPDX-License-Identifier: Apache-2.0

FROM ${{ packageVariable }}

RUN ls -al
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@
"dst": "dummy"
}
]
},
{
"id": "test-componentFour",
"type": "setup",
"files": [
{
"src": "Dockerfile",
"dst": "Dockerfile"
}
]
}
]
}

0 comments on commit d810e8c

Please sign in to comment.