Skip to content
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
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,6 @@ jobs:
- run:
name: Lint module code with NodeJS linters
command: docker compose exec -T cli bash -c "npm run lint" || [ "${VORTEX_CI_NODEJS_LINT_IGNORE_FAILURE:-0}" -eq 1 ]
#;> TOOL_ESLINT_STYLELINT

#;< DRUPAL_THEME
- run:
Expand All @@ -207,6 +206,7 @@ jobs:
[ "${VORTEX_FRONTEND_BUILD_SKIP:-0}" -eq 1 ] && exit 0
docker compose exec -T cli bash -c "npm --prefix=\${WEBROOT}/themes/custom/\${DRUPAL_THEME} run lint" || [ "${VORTEX_CI_NODEJS_LINT_IGNORE_FAILURE:-0}" -eq 1 ]
#;> DRUPAL_THEME
#;> TOOL_ESLINT_STYLELINT

# Audit job runs in its own workflow, independently of the commit workflow.
audit:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-test-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,14 @@ jobs:
- name: Lint module code with NodeJS linters
run: docker compose exec -T cli bash -c "npm run lint"
continue-on-error: ${{ vars.VORTEX_CI_NODEJS_LINT_IGNORE_FAILURE == '1' }}
#;> TOOL_ESLINT_STYLELINT

#;< DRUPAL_THEME
- name: Lint theme code with NodeJS linters
if: ${{ vars.VORTEX_FRONTEND_BUILD_SKIP != '1' }}
run: docker compose exec -T cli bash -c "npm --prefix=\${WEBROOT}/themes/custom/\${DRUPAL_THEME} run lint"
continue-on-error: ${{ vars.VORTEX_CI_NODEJS_LINT_IGNORE_FAILURE == '1' }}
#;> DRUPAL_THEME
#;> TOOL_ESLINT_STYLELINT

#;< !PROVISION_TYPE_PROFILE
database:
Expand Down
158 changes: 128 additions & 30 deletions .vortex/installer/src/Prompts/Handlers/Tools.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use AlexSkrypnyk\File\Replacer\Replacement;
use DrevOps\VortexInstaller\Utils\File;
use DrevOps\VortexInstaller\Utils\JsonManipulator;
use DrevOps\VortexInstaller\Utils\NpmLock;
use DrevOps\VortexInstaller\Utils\Strings;
use DrevOps\VortexInstaller\Utils\Yaml;
use function iter\flatten;
Expand Down Expand Up @@ -154,9 +155,7 @@ protected function processTool(string $name): void {
JsonManipulator::updateFile($this->tmpDir . '/composer.json', $tool['composer.json']);
}

if (isset($tool['package.json']) && is_callable($tool['package.json'])) {
JsonManipulator::updateFile($this->tmpDir . '/package.json', $tool['package.json']);
}
$this->processNpmManifests($tool);

if (isset($tool['ahoy'])) {
foreach ($tool['ahoy'] as $string) {
Expand All @@ -167,10 +166,57 @@ protected function processTool(string $name): void {
}
}

$this->processContent($tool);

File::removeTokenAsync('TOOL_' . strtoupper($name));
}

protected function processGroup(string $name): void {
$config = self::getToolDefinitions('groups')[$name];
$selected_tools = $this->getResponseAsArray();

if (!isset($config['tools']) || array_intersect($config['tools'], $selected_tools)) {
return;
}

$this->processNpmManifests($config);

if (isset($config['files'])) {
$files = array_map(fn($file): string => $this->tmpDir . '/' . $file, $config['files']);
File::remove($files);
}

if (isset($config['ahoy'])) {
foreach ($config['ahoy'] as $string) {
File::replaceContentInFile($this->tmpDir . '/.ahoy.yml', Replacement::create('ahoy_tool', function (string $content) use ($string): string {
$content = File::replaceContent($content, $string, '');
return Yaml::collapseEmptyLinesInLiteralBlock($content);
}));
}
}

$this->processContent($config);

if (isset($config['token'])) {
File::removeTokenAsync($config['token']);
}
}

/**
* Queue the content removals declared by a tool or a group.
*
* @param array $config
* Tool or group definition.
*/
protected function processContent(array $config): void {
if (!isset($config['strings']) && !isset($config['lines'])) {
return;
}

File::replaceContentAsync(
function (string $content, ContentFile $file) use ($tool): string {
if (isset($tool['strings'])) {
foreach ($tool['strings'] as $string) {
function (string $content, ContentFile $file) use ($config): string {
if (isset($config['strings'])) {
foreach ($config['strings'] as $string) {
if (Strings::isRegex($string)) {
$replaced = preg_replace($string, '', $content, -1, $count);

Expand All @@ -184,9 +230,9 @@ function (string $content, ContentFile $file) use ($tool): string {
}
}

if (isset($tool['lines'])) {
if (isset($config['lines'])) {
$relative_file_path = str_replace($this->tmpDir . '/', '', $file->getPathname());
foreach ($tool['lines'] as $relative_lines_file_name => $lines) {
foreach ($config['lines'] as $relative_lines_file_name => $lines) {
if ($relative_file_path === $relative_lines_file_name) {
foreach ($lines as $line) {
$content = File::removeLine($content, $line);
Expand All @@ -198,35 +244,42 @@ function (string $content, ContentFile $file) use ($tool): string {
return $content;
}
);

File::removeTokenAsync('TOOL_' . strtoupper($name));
}

protected function processGroup(string $name): void {
$config = self::getToolDefinitions('groups')[$name];
$selected_tools = $this->getResponseAsArray();

if (!isset($config['tools']) || array_intersect($config['tools'], $selected_tools)) {
return;
}

if (isset($config['files'])) {
$files = array_map(fn($file): string => $this->tmpDir . '/' . $file, $config['files']);
File::remove($files);
/**
* Apply the npm manifest edits declared by a tool or a group.
*
* @param array $config
* Tool or group definition.
*/
protected function processNpmManifests(array $config): void {
if (isset($config['package.json']) && is_callable($config['package.json'])) {
$this->updateNpmManifest($this->tmpDir . '/package.json', $config['package.json']);
}

if (isset($config['ahoy'])) {
foreach ($config['ahoy'] as $string) {
File::replaceContentInFile($this->tmpDir . '/.ahoy.yml', Replacement::create('ahoy_tool', function (string $content) use ($string): string {
$content = File::replaceContent($content, $string, '');
return Yaml::collapseEmptyLinesInLiteralBlock($content);
}));
if (isset($config['theme.package.json']) && is_callable($config['theme.package.json'])) {
foreach ($this->themeManifests() as $manifest) {
$this->updateNpmManifest($manifest, $config['theme.package.json']);
}
}
}

if (isset($config['token'])) {
File::removeTokenAsync($config['token']);
}
protected function updateNpmManifest(string $manifest, callable $callback): void {
JsonManipulator::updateFile($manifest, $callback);

// A lock file that still lists the removed dependencies makes 'npm ci'
// abort on the first build.
NpmLock::sync($manifest);
}

/**
* Find the manifests of the custom themes.
*
* @return array<int, string>
* Paths to the "package.json" files.
*/
protected function themeManifests(): array {
return glob($this->tmpDir . '/' . $this->webroot . '/themes/custom/*/package.json') ?: [];
}

public static function getToolDefinitions(string $filter = 'all'): array {
Expand Down Expand Up @@ -317,6 +370,24 @@ public static function getToolDefinitions(string $filter = 'all'): array {
$pj->addSubNode('scripts', 'lint', 'npm run lint-css');
$pj->addSubNode('scripts', 'lint-fix', 'npm run lint-fix-css');
},
'theme.package.json' => function (JsonManipulator $pj): void {
$pj->removeSubNode('devDependencies', '@eslint/compat');
$pj->removeSubNode('devDependencies', '@eslint/js');
$pj->removeSubNode('devDependencies', 'eslint');
$pj->removeSubNode('devDependencies', 'eslint-config-prettier');
$pj->removeSubNode('devDependencies', 'eslint-plugin-import');
$pj->removeSubNode('devDependencies', 'eslint-plugin-jsdoc');
$pj->removeSubNode('devDependencies', 'eslint-plugin-no-jquery');
$pj->removeSubNode('devDependencies', 'eslint-plugin-prettier');
$pj->removeSubNode('devDependencies', 'eslint-plugin-yml');
$pj->removeSubNode('devDependencies', 'globals');
$pj->removeSubNode('devDependencies', 'prettier');
$pj->removeSubNode('devDependencies', '@homer0/prettier-plugin-jsdoc');
$pj->removeSubNode('scripts', 'lint-js');
$pj->removeSubNode('scripts', 'lint-js-fix');
$pj->addSubNode('scripts', 'lint', 'npm run lint-css');
$pj->addSubNode('scripts', 'lint-fix', 'npm run lint-css-fix');
},
// A project created before the move to flat config still carries the
// legacy files, which linger unread once the tool is deselected.
'files' => ['eslint.config.mjs', '.eslintrc.json', '.eslintignore', '.prettierrc.json', '.prettierignore'],
Expand All @@ -335,6 +406,17 @@ public static function getToolDefinitions(string $filter = 'all'): array {
$pj->addSubNode('scripts', 'lint', 'npm run lint-js');
$pj->addSubNode('scripts', 'lint-fix', 'npm run lint-fix-js');
},
'theme.package.json' => function (JsonManipulator $pj): void {
$pj->removeSubNode('devDependencies', 'stylelint');
$pj->removeSubNode('devDependencies', 'stylelint-config-standard');
$pj->removeSubNode('devDependencies', 'stylelint-config-standard-scss');
$pj->removeSubNode('devDependencies', 'stylelint-order');
$pj->removeSubNode('devDependencies', 'stylelint-scss');
$pj->removeSubNode('scripts', 'lint-css');
$pj->removeSubNode('scripts', 'lint-css-fix');
$pj->addSubNode('scripts', 'lint', 'npm run lint-js');
$pj->addSubNode('scripts', 'lint-fix', 'npm run lint-js-fix');
},
'files' => ['.stylelintrc.js'],
],

Expand Down Expand Up @@ -500,9 +582,25 @@ public static function getToolDefinitions(string $filter = 'all'): array {
],
'frontend_linting' => [
'tools' => [self::ESLINT, self::STYLELINT],
// Each linter rewrites 'lint' to call the other one, so with both
// deselected the pair points at scripts that no longer exist.
'package.json' => function (JsonManipulator $pj): void {
$pj->removeSubNode('scripts', 'lint');
$pj->removeSubNode('scripts', 'lint-fix');
},
'theme.package.json' => function (JsonManipulator $pj): void {
$pj->removeSubNode('scripts', 'lint');
$pj->removeSubNode('scripts', 'lint-fix');
},
'ahoy' => [
'/^\h*ahoy cli "npm run lint"\h*\n?/m',
'/^\h*ahoy cli "npm run lint-fix"\h*\n?/m',
'ahoy cli "npm run --prefix=\${WEBROOT}/themes/custom/\${DRUPAL_THEME} lint"',
'ahoy cli "npm run --prefix=\${WEBROOT}/themes/custom/\${DRUPAL_THEME} lint-fix"',
],
'strings' => [
'/^\|\h*`npm run lint`.*\n?/m',
'/^\|\h*`npm run lint-fix`.*\n?/m',
],
'token' => 'TOOL_ESLINT_STYLELINT',
],
Expand Down
Loading
Loading