Skip to content

Commit

Permalink
add outDir param to the cli
Browse files Browse the repository at this point in the history
  • Loading branch information
shairez committed Nov 12, 2024
1 parent 9ecfa4c commit 943d7bb
Show file tree
Hide file tree
Showing 14 changed files with 275 additions and 67 deletions.
7 changes: 7 additions & 0 deletions .changeset/tiny-pants-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@builder.io/qwik': minor
---

FEAT: add monorepo support to the `qwik add` command by adding a `projectDir` param

That way you can run `qwik add --projectDir=packages/my-package` and it will add the feature to the specified project/package (sub) folder, instead of the root folder.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ jobs:
- run: pnpm install --frozen-lockfile

- name: CLI E2E Tests
run: pnpm run test.e2e-cli
run: pnpm run test.e2e.cli

########### LINT PACKAGES ############
lint-package:
Expand Down
6 changes: 3 additions & 3 deletions e2e/qwik-cli-e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This package provides isolated E2E tests by generating a new application with lo

## Description

Tests can be invoked by running `pnpm run test.e2e-cli`.
Tests can be invoked by running `pnpm run test.e2e.cli`.

**Note that running E2E tests requires the workspace projects to be prebuilt manually!**

Expand All @@ -16,8 +16,8 @@ E2E project does the following internally:

- By default `outputDir` is an auto-generated one using `tmp` npm package. The application that is created here will be removed after the test is executed
- It is possible to install into custom folder using environment variable `TEMP_E2E_PATH`. Here's how the command would look like in this case:
- with absolute path `TEMP_E2E_PATH=/Users/name/projects/tests pnpm run test.e2e-cli`
- with path relative to the qwik workspace `TEMP_E2E_PATH=temp/e2e-folder pnpm run test.e2e-cli`
- with absolute path `TEMP_E2E_PATH=/Users/name/projects/tests pnpm run test.e2e.cli`
- with path relative to the qwik workspace `TEMP_E2E_PATH=temp/e2e-folder pnpm run test.e2e.cli`

Note that provided folder should exist. If custom path is used, generated application will not be removed after the test completes, which is helpful for debugging.

Expand Down
2 changes: 1 addition & 1 deletion e2e/qwik-cli-e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
"private": true,
"scripts": {
"e2e": "vitest run --config=vite.config.ts",
"e2e:watch": "vitest watch --config=vite.config.ts"
"e2e.watch": "vitest watch --config=vite.config.ts"
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
"execa": "8.0.1",
"express": "4.20.0",
"install": "0.13.0",
"memfs": "4.14.0",
"monaco-editor": "0.45.0",
"mri": "1.2.0",
"path-browserify": "1.0.1",
Expand Down Expand Up @@ -241,7 +242,7 @@
"start": "concurrently \"npm:build.watch\" \"npm:tsc.watch\" -n build,tsc -c green,cyan",
"test": "pnpm build.full && pnpm test.unit && pnpm test.e2e",
"test.e2e": "pnpm test.e2e.chromium && pnpm test.e2e.webkit",
"test.e2e-cli": "pnpm --filter qwik-cli-e2e e2e",
"test.e2e.cli": "pnpm --filter qwik-cli-e2e e2e",
"test.e2e.chromium": "playwright test starters --browser=chromium --config starters/playwright.config.ts",
"test.e2e.chromium.debug": "PWDEBUG=1 playwright test starters --browser=chromium --config starters/playwright.config.ts",
"test.e2e.city": "playwright test starters/e2e/qwikcity --browser=chromium --config starters/playwright.config.ts",
Expand Down
16 changes: 10 additions & 6 deletions packages/qwik/src/cli/add/run-add-interactive.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { intro, isCancel, log, outro, select, spinner } from '@clack/prompts';
import { bgBlue, bgMagenta, blue, bold, cyan, magenta } from 'kleur/colors';
import type { IntegrationData, UpdateAppResult } from '../types';
import type { IntegrationData, UpdateAppOptions, UpdateAppResult } from '../types';
import { loadIntegrations, sortIntegrationsAndReturnAsClackOptions } from '../utils/integrations';
import { bye, getPackageManager, note, panic, printHeader } from '../utils/utils';
import { bye, getPackageManager, note, panic } from '../utils/utils';

/* eslint-disable no-console */
import { relative } from 'node:path';
Expand All @@ -16,8 +16,6 @@ export async function runAddInteractive(app: AppCommand, id: string | undefined)
const integrations = await loadIntegrations();
let integration: IntegrationData | undefined;

printHeader();

if (typeof id === 'string') {
// cli passed a flag with the integration id to add
integration = integrations.find((i) => i.id === id);
Expand Down Expand Up @@ -62,11 +60,17 @@ export async function runAddInteractive(app: AppCommand, id: string | undefined)
runInstall = true;
}

const result = await updateApp(pkgManager, {
const updateAppOptions: UpdateAppOptions = {
rootDir: app.rootDir,
integration: integration.id,
installDeps: runInstall,
});
};
const projectDir = app.getArg('projectDir');
if (projectDir) {
updateAppOptions.projectDir = projectDir;
}

const result = await updateApp(pkgManager, updateAppOptions);

if (app.getArg('skipConfirmation') !== 'true') {
await logUpdateAppResult(pkgManager, result);
Expand Down
20 changes: 13 additions & 7 deletions packages/qwik/src/cli/add/update-app.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { FsUpdates, UpdateAppOptions, UpdateAppResult } from '../types';
import { dirname } from 'node:path';
import { log, spinner } from '@clack/prompts';
import { bgRed, cyan } from 'kleur/colors';
import fs from 'node:fs';
import { panic } from '../utils/utils';
import { loadIntegrations } from '../utils/integrations';
import { dirname } from 'node:path';
import type { FsUpdates, UpdateAppOptions, UpdateAppResult } from '../types';
import { installDeps } from '../utils/install-deps';
import { loadIntegrations } from '../utils/integrations';
import { panic } from '../utils/utils';
import { mergeIntegrationDir } from './update-files';
import { updateViteConfigs } from './update-vite-config';
import { bgRed, cyan } from 'kleur/colors';
import { spinner, log } from '@clack/prompts';

export async function updateApp(pkgManager: string, opts: UpdateAppOptions) {
const integrations = await loadIntegrations();
Expand All @@ -29,7 +29,13 @@ export async function updateApp(pkgManager: string, opts: UpdateAppOptions) {
};
}

await mergeIntegrationDir(fileUpdates, opts, integration.dir, opts.rootDir);
await mergeIntegrationDir(
fileUpdates,
opts,
integration.dir,
opts.rootDir,
integration.alwaysInRoot
);

if ((globalThis as any).CODE_MOD) {
await updateViteConfigs(fileUpdates, integration, opts.rootDir);
Expand Down
60 changes: 48 additions & 12 deletions packages/qwik/src/cli/add/update-files.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,54 @@
import fs from 'node:fs';
import type { FsUpdates, UpdateAppOptions } from '../types';
import { extname, join } from 'node:path';
import type { FsUpdates, UpdateAppOptions } from '../types';
import { getPackageManager } from '../utils/utils';

export async function mergeIntegrationDir(
fileUpdates: FsUpdates,
opts: UpdateAppOptions,
srcDir: string,
destDir: string
destDir: string,
alwaysInRoot?: string[]
) {
const items = await fs.promises.readdir(srcDir);
await Promise.all(
items.map(async (itemName) => {
const destName = itemName === 'gitignore' ? '.gitignore' : itemName;
const ext = extname(destName);
const srcChildPath = join(srcDir, itemName);
const destChildPath = join(destDir, destName);

const destRootPath = join(destDir, destName);

const s = await fs.promises.stat(srcChildPath);

if (s.isDirectory()) {
await mergeIntegrationDir(fileUpdates, opts, srcChildPath, destChildPath);
await mergeIntegrationDir(fileUpdates, opts, srcChildPath, destRootPath, alwaysInRoot);
} else if (s.isFile()) {
const finalDestPath = getFinalDestPath(opts, destRootPath, destDir, destName, alwaysInRoot);

if (destName === 'package.json') {
await mergePackageJsons(fileUpdates, srcChildPath, destChildPath);
await mergePackageJsons(fileUpdates, srcChildPath, destRootPath);
} else if (destName === 'settings.json') {
await mergeJsons(fileUpdates, srcChildPath, destChildPath);
await mergeJsons(fileUpdates, srcChildPath, finalDestPath);
} else if (destName === 'README.md') {
await mergeReadmes(fileUpdates, srcChildPath, destChildPath);
await mergeReadmes(fileUpdates, srcChildPath, finalDestPath);
} else if (
destName === '.gitignore' ||
destName === '.prettierignore' ||
destName === '.eslintignore'
) {
await mergeIgnoresFile(fileUpdates, srcChildPath, destChildPath);
await mergeIgnoresFile(fileUpdates, srcChildPath, destRootPath);
} else if (ext === '.css') {
await mergeCss(fileUpdates, srcChildPath, destChildPath, opts);
} else if (fs.existsSync(destChildPath)) {
await mergeCss(fileUpdates, srcChildPath, finalDestPath, opts);
} else if (fs.existsSync(finalDestPath)) {
fileUpdates.files.push({
path: destChildPath,
path: finalDestPath,
content: await fs.promises.readFile(srcChildPath),
type: 'overwrite',
});
} else {
fileUpdates.files.push({
path: destChildPath,
path: finalDestPath,
content: await fs.promises.readFile(srcChildPath),
type: 'create',
});
Expand All @@ -53,6 +58,37 @@ export async function mergeIntegrationDir(
);
}

function getFinalDestPath(
opts: UpdateAppOptions,
destRootPath: string,
destDir: string,
destName: string,
alwaysInRoot?: string[]
) {
// If the integration has a projectDir, copy the files to the projectDir
// Unless that path is part of "alwaysInRoot"
const projectDir = opts.projectDir ? opts.projectDir : '';
const destDirParts = destDir.split('/');
const rootDirIndex = destDirParts.indexOf(opts.rootDir);
const destChildPath =
rootDirIndex !== -1
? join(
...destDirParts.slice(0, rootDirIndex + 1),
projectDir,
...destDirParts.slice(rootDirIndex + 1),
destName
)
: join(destDir, projectDir, destName);

const finalDestPath =
alwaysInRoot &&
alwaysInRoot.some((rootItem) => destName.includes(rootItem) || destDir.includes(rootItem))
? destRootPath
: destChildPath;

return finalDestPath;
}

async function mergePackageJsons(fileUpdates: FsUpdates, srcPath: string, destPath: string) {
const srcContent = await fs.promises.readFile(srcPath, 'utf-8');
try {
Expand Down
94 changes: 94 additions & 0 deletions packages/qwik/src/cli/add/update-files.unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { fs } from 'memfs';
import { join } from 'path';
import { describe, expect, test, vi } from 'vitest';
import type { FsUpdates, UpdateAppOptions } from '../types';
import { mergeIntegrationDir } from './update-files';

vi.mock('node:fs', () => ({
default: fs,
}));

function setup() {
const fakeSrcDir = 'srcDir';
createFakeFiles(fakeSrcDir);

const fakeDestDir = 'destDir';

const fakeFileUpdates: FsUpdates = {
files: [],
installedDeps: {},
installedScripts: [],
};

const fakeOpts: UpdateAppOptions = {
rootDir: fakeDestDir,
integration: 'integration',
};

return {
fakeSrcDir,
fakeDestDir,
fakeFileUpdates,
fakeOpts,
};
}

describe('mergeIntegrationDir', () => {
test('should merge integration directory', async () => {
const { fakeSrcDir, fakeDestDir, fakeFileUpdates, fakeOpts } = setup();

await mergeIntegrationDir(fakeFileUpdates, fakeOpts, fakeSrcDir, fakeDestDir);

const actualResults = fakeFileUpdates.files.map((f) => f.path);
const expectedResult = ['destDir/fake.ts', 'destDir/package.json', 'destDir/src/global.css'];

expect(actualResults).toEqual(expectedResult);
});

test('should merge integration directory in a monorepo', async () => {
const { fakeSrcDir, fakeDestDir, fakeFileUpdates, fakeOpts } = setup();

// Create a global file in the destination director
const monorepoSubDir = join(fakeDestDir, 'apps', 'subpackage', 'src');
fs.mkdirSync(monorepoSubDir, { recursive: true });
fs.writeFileSync(join(monorepoSubDir, 'global.css'), '/* CSS */');

// Add a file that should stay in the root
fs.writeFileSync(join(fakeSrcDir, 'should-stay-in-root.ts'), 'fake file');

// Creating a folder that should stay in the root
fs.mkdirSync(join(fakeSrcDir, 'should-stay'), { recursive: true });
fs.writeFileSync(join(fakeSrcDir, 'should-stay', 'should-also-stay.ts'), 'fake file');

fakeOpts.projectDir = 'apps/subpackage';
fakeOpts.installDeps = true;
const fakeAlwaysInRoot = ['should-stay-in-root.ts', 'should-stay'];

await mergeIntegrationDir(fakeFileUpdates, fakeOpts, fakeSrcDir, fakeDestDir, fakeAlwaysInRoot);

const actualResults = fakeFileUpdates.files.map((f) => f.path);
const expectedResult = [
`destDir/apps/subpackage/fake.ts`,
`destDir/should-stay-in-root.ts`,
`destDir/package.json`,
`destDir/should-stay/should-also-stay.ts`,
`destDir/apps/subpackage/src/global.css`,
];

expect(actualResults).toEqual(expectedResult);

const actualGlobalCssContent = fakeFileUpdates.files.find(
(f) => f.path === `destDir/apps/subpackage/src/global.css`
)?.content;

expect(actualGlobalCssContent).toBe('p{color: red}\n\n/* CSS */\n');
});
});

function createFakeFiles(dir: string) {
// Create fake src files
fs.mkdirSync(join(dir, 'src'), { recursive: true });
fs.writeFileSync(join(dir, 'fake.ts'), 'fake file');
fs.writeFileSync(join(dir, 'package.json'), '{"name": "fake"}');
fs.writeFileSync(join(dir, 'src', 'global.css'), 'p{color: red}');
}
22 changes: 14 additions & 8 deletions packages/qwik/src/cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface UpdateAppOptions {
rootDir: string;
integration: string;
installDeps?: boolean;
projectDir?: string;
}

export interface UpdateAppResult {
Expand Down Expand Up @@ -43,6 +44,8 @@ export interface IntegrationData {
priority: number;
docs: string[];
viteConfig?: ViteConfigUpdates;
// Files and folders that should be copied to root ignoring `projectDir`
alwaysInRoot?: string[];
}

export type IntegrationType = 'app' | 'feature' | 'adapter';
Expand Down Expand Up @@ -77,14 +80,17 @@ export interface IntegrationPackageJson {
qwikTemplates?: string[];
types?: string;
type?: string;
__qwik__?: {
displayName?: string;
nextSteps?: NextSteps;
docs?: string[];
priority: number;
postInstall?: string;
viteConfig?: ViteConfigUpdates;
};
__qwik__?: QwikIntegrationConfig;
}

export interface QwikIntegrationConfig {
displayName?: string;
nextSteps?: NextSteps;
docs?: string[];
priority: number;
postInstall?: string;
viteConfig?: ViteConfigUpdates;
alwaysInRoot?: string[];
}

export interface EnsureImport {
Expand Down
3 changes: 2 additions & 1 deletion packages/qwik/src/cli/utils/integrations.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import { join } from 'node:path';
import type { IntegrationData, IntegrationType } from '../types';
import { dashToTitleCase, readPackageJson, limitLength } from './utils';
import { dashToTitleCase, limitLength, readPackageJson } from './utils';

let integrations: IntegrationData[] | null = null;

Expand Down Expand Up @@ -55,6 +55,7 @@ export async function loadIntegrations() {
pkgJson,
docs: pkgJson.__qwik__?.docs ?? [],
priority: pkgJson?.__qwik__?.priority ?? 0,
alwaysInRoot: pkgJson.__qwik__?.alwaysInRoot ?? [],
};
loadingIntegrations.push(integration);
}
Expand Down
Loading

0 comments on commit 943d7bb

Please sign in to comment.