diff --git a/configuration/gitignore.ts b/configuration/gitignore.ts index e5126f0..1a00bc6 100644 --- a/configuration/gitignore.ts +++ b/configuration/gitignore.ts @@ -2,6 +2,7 @@ import type { Template } from '../types' export const templates: Template = { recommended: ['node_modules', 'bun.lockb'], + bundle: ['node_modules', 'bun.lockb', 'dist'], } export function createFile(values: string[]) { diff --git a/configuration/rsbuild.ts b/configuration/rsbuild.ts index d885fba..b6ba1ba 100644 --- a/configuration/rsbuild.ts +++ b/configuration/rsbuild.ts @@ -1,4 +1,18 @@ import { fileExtension, state } from '../state' +import type { Template } from '../types' + +export const templates: Template = { + web: { + tools: { + rspack: { + resolve: { + // Resolve absolute imports relative to project root first. + modules: ['.', 'node_modules'], + }, + }, + }, + }, +} export const extension = (path: string) => ({ extends: path }) diff --git a/configuration/typescript.ts b/configuration/typescript.ts index c8718a5..9d606b1 100644 --- a/configuration/typescript.ts +++ b/configuration/typescript.ts @@ -8,9 +8,22 @@ export const templates = { compilerOptions: { strict: true, skipLibCheck: true, + verbatimModuleSyntax: true, target: 'ES2020', lib: ['DOM', 'ES2020'], module: 'Preserve', + noEmit: true, + }, + }, + web: { + compilerOptions: { + skipLibCheck: true, + baseUrl: '.', + target: 'ESNext', + lib: ['DOM', 'ESNext'], + module: 'Preserve', + jsx: 'react-jsx', + noEmit: true, }, }, } diff --git a/configuration/vscode.ts b/configuration/vscode.ts index 2366b46..0bb4908 100644 --- a/configuration/vscode.ts +++ b/configuration/vscode.ts @@ -12,7 +12,7 @@ export const templates: Template = { 'prettier-eslint': { 'editor.defaultFormatter': 'esbenp.prettier-vscode', 'editor.codeActionsOnSave': { - 'source.fixAll.eslint': true, + 'source.fixAll.eslint': 'always', }, 'editor.formatOnSave': true, }, diff --git a/helper.ts b/helper.ts index f043580..398a2ff 100644 --- a/helper.ts +++ b/helper.ts @@ -1,6 +1,7 @@ -import { existsSync, lstatSync } from 'node:fs' +import { existsSync, lstatSync, symlinkSync } from 'node:fs' import { it } from 'avait' -import Bun, { Glob } from 'bun' +import Bun from 'bun' +import glob from 'fast-glob' import { create } from 'logua' import { parse } from 'parse-gitignore' import { z } from 'zod' @@ -28,21 +29,20 @@ export const validate = (configuration: unknown) => { } export async function findConfiguration() { - const packageJson = await Bun.file(root('./package.json')).json() const { value: typeScriptModuleContents } = await it(import(root('./configuration.ts'))) const { value: javaScriptModuleContents } = await it(import(root('./configuration.js'))) - if (!(typeScriptModuleContents || javaScriptModuleContents || Object.hasOwn(packageJson, 'configuration'))) { + if (!(typeScriptModuleContents || javaScriptModuleContents || Object.hasOwn(state.packageJson, 'configuration'))) { log('No configuration found', 'error') } - if (!packageJson.configuration && typeScriptModuleContents) { + if (!state.packageJson.configuration && typeScriptModuleContents) { state.language = 'typescript' - } else if (!packageJson.configuration && javaScriptModuleContents) { + } else if (!state.packageJson.configuration && javaScriptModuleContents) { state.language = 'javascript' } - const userConfiguration = packageJson.configuration ?? typeScriptModuleContents ?? javaScriptModuleContents + const userConfiguration = state.packageJson.configuration ?? typeScriptModuleContents ?? javaScriptModuleContents validate(userConfiguration) state.options = userConfiguration } @@ -95,9 +95,15 @@ export async function getWorkspaces() { if (Array.isArray(packageJson.workspaces)) { for (const workspaceGlob of packageJson.workspaces) { - const glob = new Glob(workspaceGlob) - for await (const file of glob.scan({ cwd: root('/'), dot: false, onlyFiles: false })) { - if (lstatSync(file).isDirectory()) { + const files = await glob(workspaceGlob, { + cwd: root('/'), + dot: false, + onlyFiles: false, + followSymbolicLinks: false, + ignore: ['node_modules'], + }) + for await (const file of files) { + if (lstatSync(root(file)).isDirectory()) { workspaces.push({ path: file, root: false }) } } @@ -108,3 +114,25 @@ export async function getWorkspaces() { return workspaces } + +export function installLocalDependencies() { + const { localDependencies } = state.packageJson + if (!localDependencies || typeof localDependencies !== 'object' || Object.keys(localDependencies).length === 0) { + return + } + + for (const [name, folder] of Object.entries(localDependencies)) { + const absolutePath = root(folder) + const targetPath = root(`node_modules/${name}`) + if (existsSync(absolutePath) && !existsSync(targetPath)) { + try { + symlinkSync(absolutePath, targetPath) + } catch (_error) { + // Symlinks only allowed for administrators on Windows. + log(`Failed to create symlink for localDependency ${name}`, 'warning') + } + } else if (!existsSync(absolutePath)) { + log(`localDependency "${name}" is pointing to a non-existing location: ${absolutePath}`, 'warning') + } + } +} diff --git a/index.ts b/index.ts index d1c641a..772c7ec 100644 --- a/index.ts +++ b/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun import Bun from 'bun' import { configurations } from './configuration' -import { findConfiguration, getWorkspaces, log, writeGitIgnore } from './helper' +import { findConfiguration, getWorkspaces, installLocalDependencies, log, writeGitIgnore } from './helper' import { parse } from './parse' import { reset, root, state } from './state' @@ -24,9 +24,12 @@ async function configureProject() { if (!gitUserConfigured && ignores.length === 0) { log('No configuration to add', 'warning') } + + installLocalDependencies() } for (const workspace of await getWorkspaces()) { - reset(workspace) + const packageJson = await Bun.file(root('./package.json')).json() + reset(workspace, packageJson) await configureProject() } diff --git a/package.json b/package.json index 4d838dc..17a96da 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "avait": "^1.0.0", + "fast-glob": "^3.3.2", "logua": "^3.0.3", "parse-gitignore": "^2.0.0", "ts-deepmerge": "^7.0.0", @@ -59,6 +60,9 @@ "!test/fixture/workspaces/demo/react", "test/fixture/workspaces/demo/react/*", "!test/fixture/workspaces/demo/react/package.json", + "!test/fixture/local-dependencies/node_modules", + "test/fixture/local-dependencies/node_modules/*", + "!test/fixture/local-dependencies/node_modules/keep", "!test/fixture/*/configuration.ts", "!test/fixture/*/configuration.js" ], diff --git a/state.ts b/state.ts index babf3eb..60151dd 100644 --- a/state.ts +++ b/state.ts @@ -1,5 +1,5 @@ import { join } from 'node:path' -import type { State } from './types' +import type { PackageJson, State } from './types' export const state: State = { options: {}, @@ -14,10 +14,10 @@ export const fileExtension = () => (state.language === 'javascript' ? 'js' : 'ts export const root = (file: string) => process.cwd().includes('node_modules') ? join(process.cwd(), '../..', state.directory, file) : join(process.cwd(), state.directory, file) -export const reset = ({ path, root }: { path: string; root: boolean }) => { +export const reset = ({ path, root }: { path: string; root: boolean }, packageJson: PackageJson) => { state.options = {} state.language = 'json' - state.packageJson = { name: 'missing-package-name' } + state.packageJson = packageJson state.directory = path state.root = root } diff --git a/test/basic.test.ts b/test/basic.test.ts index 3437722..f8cbde9 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -95,3 +95,16 @@ test('Creates configuration files in all workspaces including the root.', () => expect(existsSync(join(fixturePath, 'demo/react/tsconfig.json'))).toBe(true) expect(existsSync(join(fixturePath, 'plugin/tsconfig.json'))).toBe(true) }) + +test('Will also install local dependencies if listed.', () => { + const fixturePath = './test/fixture/local-dependencies' + + execSync('bun ./../../../index.ts', { + cwd: fixturePath, + stdio: 'inherit', + }) + + expect(existsSync(join(fixturePath, 'tsconfig.json'))).toBe(true) + expect(existsSync(join(fixturePath, 'node_modules/keep/package.json'))).toBe(true) + expect(existsSync(join(fixturePath, 'node_modules/empty-dependency/package.json'))).toBe(true) +}) diff --git a/test/fixture/local-dependencies/node_modules/keep/package.json b/test/fixture/local-dependencies/node_modules/keep/package.json new file mode 100644 index 0000000..bac36f5 --- /dev/null +++ b/test/fixture/local-dependencies/node_modules/keep/package.json @@ -0,0 +1,3 @@ +{ + "name": "keep" +} \ No newline at end of file diff --git a/test/fixture/local-dependencies/package.json b/test/fixture/local-dependencies/package.json new file mode 100644 index 0000000..27ef24f --- /dev/null +++ b/test/fixture/local-dependencies/package.json @@ -0,0 +1,9 @@ +{ + "name": "local-dependencies", + "localDependencies": { + "empty-dependency": "../empty" + }, + "configuration": { + "typescript": "web" + } +} \ No newline at end of file diff --git a/types.ts b/types.ts index d1c809d..678b07f 100644 --- a/types.ts +++ b/types.ts @@ -2,7 +2,14 @@ import type { ConfigurationKeys } from './configuration' export type Template = { [key: string]: T | (() => T) } -export type PackageJson = { name: string; author?: string | { name: string } } +type Dependencies = { [key: string]: string } + +export type PackageJson = { + name: string + author?: string | { name: string } + localDependencies?: Dependencies + configuration?: { [key: string]: string | object | string[] } +} export type Configuration = { name: ConfigurationKeys