Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Additional files for transform #278

Merged
merged 8 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
}
]
}
]
}