diff --git a/packages/-warp-drive/package.json b/packages/-warp-drive/package.json index ffaf825ea35..67e45247acd 100644 --- a/packages/-warp-drive/package.json +++ b/packages/-warp-drive/package.json @@ -1,7 +1,7 @@ { "name": "warp-drive", "version": "0.1.0-alpha.1", - "private": true, + "private": false, "description": "WarpDrive is the data framework for ambitious web applications", "keywords": [ "typescript", @@ -53,7 +53,8 @@ "@embroider/macros": "^1.16.1", "@warp-drive/build-config": "workspace:0.0.0-alpha.15", "semver": "^7.6.2", - "chalk": "^5.3.0" + "chalk": "^5.3.0", + "comment-json": "^4.2.3" }, "devDependencies": { "@babel/core": "^7.24.5", diff --git a/packages/-warp-drive/src/-private/shared/git.ts b/packages/-warp-drive/src/-private/shared/git.ts index 98e07a43878..21df7723859 100644 --- a/packages/-warp-drive/src/-private/shared/git.ts +++ b/packages/-warp-drive/src/-private/shared/git.ts @@ -1,17 +1,13 @@ import chalk from 'chalk'; -import { - branchForChannelAndVersion, - CHANNEL, - channelForBranch, - npmDistTagForChannelAndVersion, - SEMVER_VERSION, - VALID_BRANCHES, -} from './channel'; -import { getFile } from './json-file'; -import { exec } from './cmd'; -import { gatherPackages, loadStrategy, Package } from './package'; import path from 'path'; +import type { CHANNEL, SEMVER_VERSION, VALID_BRANCHES } from './channel'; +import { branchForChannelAndVersion, channelForBranch, npmDistTagForChannelAndVersion } from './channel'; +import { exec } from './cmd'; +import { getFile } from './json-file'; +import type { Package } from './package'; +import { gatherPackages, loadStrategy } from './package'; + export type LTS_TAG = `lts-${number}-${number}`; export type RELEASE_TAG = `release-${number}-${number}`; export type GIT_TAG = @@ -192,7 +188,7 @@ export async function getAllPackagesForGitTag(tag: GIT_TAG): Promise; + version: string; + dependencies: Record; + devDependencies: Record; + peerDependencies: Record; + peerDependenciesMeta: Record; +}; + +const InfoCache: Record = {}; + +// eslint-disable-next-line @typescript-eslint/require-await +export async function exec(cmd: string) { + return execSync(cmd); +} + +export async function getTags(project: string): Promise> { + if (!InfoCache[project]) { + const info = await exec(`npm view ${project} --json`); + InfoCache[project] = JSON.parse(String(info)) as unknown as NpmInfo; + } + + const keys = Object.keys(InfoCache[project]['dist-tags']); + return new Set(keys); +} + +export async function getInfo(project: string): Promise { + if (!InfoCache[project]) { + const info = await exec(`npm view ${project} --json`); + InfoCache[project] = JSON.parse(String(info)) as unknown as NpmInfo; + } + + return InfoCache[project]; +} + +export function getPackageManagerFromLockfile(): 'yarn' | 'npm' | 'bun' | 'pnpm' { + if (fs.existsSync('pnpm-lock.yaml')) { + return 'pnpm'; + } else if (fs.existsSync('package-lock.json')) { + return 'npm'; + } else if (fs.existsSync('yarn.lock')) { + return 'yarn'; + } else if (fs.existsSync('bun.lock')) { + return 'bun'; + } + return 'npm'; +} diff --git a/packages/-warp-drive/src/-private/shared/parse-args.ts b/packages/-warp-drive/src/-private/shared/parse-args.ts index 44ef9c8faf0..4bc9c77cd13 100644 --- a/packages/-warp-drive/src/-private/shared/parse-args.ts +++ b/packages/-warp-drive/src/-private/shared/parse-args.ts @@ -30,7 +30,12 @@ export type Command = { load: () => Promise; }; -export type CmdFn = (args: string[]) => Promise; +type FlagPrimitiveValue = string | number | boolean | null; +export type ParsedFlags = { + full: Map; + specified: Map; +}; +export type CmdFn = (flags: ParsedFlags) => Promise; export type CommandConfig = Record; export type Flag = { @@ -39,7 +44,7 @@ export type Flag = { flag_aliases?: string[]; flag_mispellings?: string[]; description: string; - validate?: (value: unknown, config: Map) => void | Promise; + validate?: (value: unknown, config: Map) => void | Promise; examples: Array< | string | { @@ -53,9 +58,7 @@ export type Flag = { | number | boolean | null - | (( - config: Map - ) => string | number | boolean | null | Promise); + | ((config: Map) => FlagPrimitiveValue | Promise); /* Positional flags are not specified by name, but by position When using this with more than one positional flag, you must specify positional_index @@ -101,7 +104,7 @@ export type BinConfig = { const FalseyStrings = new Set(['false', '0', 'no', 'n', 'off', '']); -function processRawValue(config: Flag, raw_value: string | undefined): string | number | boolean | null { +function processRawValue(config: Flag, raw_value: string | undefined): FlagPrimitiveValue { if (raw_value === undefined) { if (config.type === Boolean) { return config.invert_boolean ? false : true; @@ -120,10 +123,7 @@ function processRawValue(config: Flag, raw_value: string | undefined): string | } } -async function processMissingFlag( - config: Flag, - values: Map -): Promise { +async function processMissingFlag(config: Flag, values: Map): Promise { if (config.default_value !== undefined) { if (typeof config.default_value === 'function') { return await config.default_value(values); @@ -384,6 +384,11 @@ export function getCommands(), + specified: new Map(), +}; + export async function runBinCommand(config: BinConfig): Promise { const args = process.argv.slice(2); @@ -404,6 +409,8 @@ export async function runBinCommand(config: BinConfig): Promise { } const cmdFn = await cmd.load(); - await cmdFn(args); + const flags = cmd.options ? await parseRawFlags(args, cmd.options) : EMPTY_FLAGS; + + await cmdFn(flags); process.exit(0); } diff --git a/packages/-warp-drive/src/-private/shared/the-big-list.ts b/packages/-warp-drive/src/-private/shared/the-big-list.ts new file mode 100644 index 00000000000..63668da9a6f --- /dev/null +++ b/packages/-warp-drive/src/-private/shared/the-big-list.ts @@ -0,0 +1,92 @@ +export const Main = [ + '@ember-data/active-record', + '@ember-data/adapter', + '@ember-data/codemods', + '@ember-data/debug', + '@ember-data/graph', + '@ember-data/json-api', + '@ember-data/legacy-compat', + '@ember-data/model', + '@ember-data/request-utils', + '@ember-data/request', + '@ember-data/rest', + '@ember-data/serializer', + '@ember-data/store', + '@ember-data/tracking', + '@warp-drive/build-config', + '@warp-drive/core-types', + '@warp-drive/diagnostic', + '@warp-drive/ember', + '@warp-drive/holodeck', + '@warp-drive/schema-record', + '@warp-drive/schema', + 'ember-data', + 'eslint-plugin-ember-data', + 'warp-drive', +]; + +export const Types = [ + '@ember-data-types/active-record', + '@ember-data-types/adapter', + '@ember-data-types/graph', + '@ember-data-types/json-api', + '@ember-data-types/legacy-compat', + '@ember-data-types/model', + '@ember-data-types/request-utils', + '@ember-data-types/request', + '@ember-data-types/rest', + '@ember-data-types/serializer', + '@ember-data-types/store', + '@ember-data-types/tracking', + '@warp-drive-types/core-types', + 'ember-data-types', +]; + +export const Mirror = [ + '@ember-data-mirror/active-record', + '@ember-data-mirror/adapter', + '@ember-data-mirror/graph', + '@ember-data-mirror/json-api', + '@ember-data-mirror/legacy-compat', + '@ember-data-mirror/model', + '@ember-data-mirror/request-utils', + '@ember-data-mirror/request', + '@ember-data-mirror/rest', + '@ember-data-mirror/serializer', + '@ember-data-mirror/store', + '@ember-data-mirror/tracking', + '@warp-drive-mirror/build-config', + '@warp-drive-mirror/core-types', + '@warp-drive-mirror/schema-record', + 'ember-data-mirror', +]; + +export const DefinitelyTyped = [ + '@types/ember', + '@types/ember-data', + '@types/ember-data__adapter', + '@types/ember-data__model', + '@types/ember-data__serializer', + '@types/ember-data__store', + '@types/ember__application', + '@types/ember__array', + '@types/ember__component', + '@types/ember__controller', + '@types/ember__debug', + '@types/ember__destroyable', + '@types/ember__engine', + '@types/ember__error', + '@types/ember__helper', + '@types/ember__modifier', + '@types/ember__object', + '@types/ember__owner', + '@types/ember__routing', + '@types/ember__runloop', + '@types/ember__service', + '@types/ember__string', + '@types/ember__template', + '@types/ember__test', + '@types/ember__utils', +]; + +export const ALL = ([] as string[]).concat(Main, Types, Mirror); diff --git a/packages/-warp-drive/src/-private/shared/utils.ts b/packages/-warp-drive/src/-private/shared/utils.ts index 5b033918615..f83ef755e75 100644 --- a/packages/-warp-drive/src/-private/shared/utils.ts +++ b/packages/-warp-drive/src/-private/shared/utils.ts @@ -1,4 +1,8 @@ import chalk from 'chalk'; +import fs from 'fs'; +import path from 'path'; + +import type { SEMVER_VERSION } from './channel'; /** * Like Pick but returns an object type instead of a union type. @@ -243,3 +247,55 @@ export function write(out: string): void { // eslint-disable-next-line no-console console.log(out); } + +type ExportConfig = Record>>; + +export type PACKAGEJSON = { + name: string; + version: SEMVER_VERSION; + private: boolean; + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + scripts?: Record; + files?: string[]; + exports?: ExportConfig; + 'ember-addon'?: { + main?: 'addon-main.js'; + type?: 'addon'; + version?: 1 | 2; + }; + author?: string; + license?: string; + repository?: { + type: string; + url: string; + directory?: string; + }; +}; + +export function getPkgJson() { + const file = fs.readFileSync(path.join(process.cwd(), 'package.json'), { encoding: 'utf-8' }); + const json = JSON.parse(file) as PACKAGEJSON; + return json; +} + +export function writePkgJson(json: PACKAGEJSON) { + fs.writeFileSync(path.join(process.cwd(), 'package.json'), JSON.stringify(json, null, 2) + '\n'); +} + +export function getLocalPkgJson(name: string) { + const file = fs.readFileSync(path.join(process.cwd(), 'node_modules', name, 'package.json'), { encoding: 'utf-8' }); + const json = JSON.parse(file) as PACKAGEJSON; + return json; +} + +export function getTypePathFor(name: string) { + const pkg = getLocalPkgJson(name); + const isAlpha = pkg.files?.includes('unstable-preview-types'); + const isBeta = pkg.files?.includes('preview-types'); + const base = pkg.exports?.['.']; + const isStable = !isAlpha && !isBeta && typeof base === 'object' && base?.['types'] !== undefined; + + return isAlpha ? 'unstable-preview-types' : isBeta ? 'preview-types' : isStable ? 'types' : null; +} diff --git a/packages/-warp-drive/src/-private/warp-drive/cmd.config.ts b/packages/-warp-drive/src/-private/warp-drive/cmd.config.ts index db1f630a985..5124b24df11 100644 --- a/packages/-warp-drive/src/-private/warp-drive/cmd.config.ts +++ b/packages/-warp-drive/src/-private/warp-drive/cmd.config.ts @@ -1,3 +1,4 @@ +import { getTags } from '../shared/npm.ts'; import type { CommandConfig, FlagConfig } from '../shared/parse-args.ts'; export const INSTALL_OPTIONS: FlagConfig = { @@ -27,6 +28,7 @@ export const INSTALL_OPTIONS: FlagConfig = { }, }; +const RETROFIT_COMMANDS = ['types', 'mirror']; export const RETROFIT_OPTIONS: FlagConfig = { help: { name: 'Help', @@ -52,6 +54,99 @@ export const RETROFIT_OPTIONS: FlagConfig = { description: 'Print this usage manual.', examples: ['npx warp-drive retrofit --help'], }, + command_string: { + name: 'Command String', + flag: 'command_string', + type: String, + description: ' positional shorthand for fits that take a version arg', + examples: [], + default_value() { + return null; + }, + validate: async (value: unknown) => { + if (typeof value !== 'string') { + throw new Error(`Expected to be a string`); + } + const [cmd, version] = value.split('@'); + + if (!RETROFIT_COMMANDS.includes(cmd)) { + throw new Error(`Command in (${value}) must be one of ${RETROFIT_COMMANDS.join(', ')}`); + } + + if (!version && !value.includes('@')) { + return; + } + + const distTags = await getTags('ember-data'); + if (!distTags.has(version)) { + throw new Error(`version in (${value}) must be a valid NPM dist-tag`); + } + }, + positional: true, + positional_index: 0, + }, + fit: { + name: 'Fit', + flag: 'fit', + type: String, + description: '', + examples: [], + default_value: (options: Map) => { + const cmdString = options.get('command_string'); + if (!cmdString || typeof cmdString !== 'string') { + throw new Error(`Must specify a fit to retrofit`); + } + const [cmd] = cmdString.split('@'); + + if (!RETROFIT_COMMANDS.includes(cmd)) { + throw new Error(`Command in (${cmdString}) must be one of ${RETROFIT_COMMANDS.join(', ')}`); + } + + return cmd; + }, + validate: (value: unknown) => { + if (!value || typeof value !== 'string' || !RETROFIT_COMMANDS.includes(value)) { + throw new Error(`Command (${value as string}) must be one of ${RETROFIT_COMMANDS.join(', ')}`); + } + }, + }, + + version: { + name: 'Version', + flag: 'version', + type: String, + description: '', + examples: [], + default_value: async (options: Map) => { + const cmdString = options.get('command_string'); + if (!cmdString || typeof cmdString !== 'string') { + throw new Error(`Must specify a fit to retrofit`); + } + const [, version] = cmdString.split('@'); + + if (!version) { + throw new Error(`Expected a version to be included in `); + } + + const distTags = await getTags('ember-data'); + if (!distTags.has(version)) { + throw new Error(`version in (${version}) must be a valid NPM dist-tag`); + } + + return version; + }, + validate: async (value: unknown) => { + if (!value || typeof value !== 'string') { + throw new Error(`version must be a string`); + } + const distTags = await getTags('ember-data'); + if (!distTags.has(value)) { + throw new Error( + `version (${value}) must be a valid NPM dist-tag: available ${Array.from(distTags).join(', ')}` + ); + } + }, + }, }; export const COMMANDS: CommandConfig = { @@ -91,6 +186,15 @@ export const COMMANDS: CommandConfig = { options: RETROFIT_OPTIONS, load: () => import('./commands/retrofit.ts').then((v) => v.retrofit), }, + eject: { + name: 'Eject', + cmd: 'eject', + description: + 'Removes the ember-data package from your project, installing and configuring individual dependencies instead', + alt: [], + options: {}, + load: () => import('./commands/eject.ts').then((v) => v.eject), + }, }; export const Bin = { diff --git a/packages/-warp-drive/src/-private/warp-drive/commands/default-ts-config.js b/packages/-warp-drive/src/-private/warp-drive/commands/default-ts-config.js new file mode 100644 index 00000000000..03b7a360fad --- /dev/null +++ b/packages/-warp-drive/src/-private/warp-drive/commands/default-ts-config.js @@ -0,0 +1,26 @@ +export const TS_CONFIG = { + include: ['app/**/*', 'config/**/*', 'tests/**/*'], + compilerOptions: { + lib: ['DOM', 'ESNext'], + module: 'esnext', + target: 'esnext', + moduleResolution: 'bundler', + moduleDetection: 'force', + strict: true, + pretty: true, + exactOptionalPropertyTypes: false, + downlevelIteration: true, + skipLibCheck: true, + allowSyntheticDefaultImports: true, + forceConsistentCasingInFileNames: true, + allowJs: true, + baseUrl: '.', + noImplicitOverride: false, + noImplicitAny: false, + experimentalDecorators: true, + incremental: true, + noEmit: true, + declaration: false, + types: [], + }, +}; diff --git a/packages/-warp-drive/src/-private/warp-drive/commands/eject.ts b/packages/-warp-drive/src/-private/warp-drive/commands/eject.ts new file mode 100644 index 00000000000..13813709833 --- /dev/null +++ b/packages/-warp-drive/src/-private/warp-drive/commands/eject.ts @@ -0,0 +1 @@ +export async function eject() {} diff --git a/packages/-warp-drive/src/-private/warp-drive/commands/retrofit.ts b/packages/-warp-drive/src/-private/warp-drive/commands/retrofit.ts index 0344bce4482..e91a4408f31 100644 --- a/packages/-warp-drive/src/-private/warp-drive/commands/retrofit.ts +++ b/packages/-warp-drive/src/-private/warp-drive/commands/retrofit.ts @@ -1 +1,369 @@ -export async function retrofit() {} +import chalk from 'chalk'; +import JSONC from 'comment-json'; +import fs from 'fs'; +import path from 'path'; + +import { exec, getInfo, getPackageManagerFromLockfile, getTags } from '../../shared/npm'; +import type { ParsedFlags } from '../../shared/parse-args'; +import { ALL, DefinitelyTyped, Main, Mirror, Types } from '../../shared/the-big-list'; +import { getPkgJson, getTypePathFor, write, writePkgJson } from '../../shared/utils'; +import { TS_CONFIG } from './default-ts-config'; + +function assertIsString(value: unknown): asserts value is T { + if (!value || typeof value !== 'string') { + throw new Error(`Expected value ${value as string} to be a string`); + } +} + +type RetrofitTypes = 'types' | 'mirror'; + +export async function retrofit(flags: ParsedFlags) { + const fit = flags.full.get('fit'); + assertIsString(fit); + + switch (fit) { + case 'mirror': + throw new Error('Not Implemented'); + case 'types': + return await retrofitTypes(flags.full); + default: + throw new Error(`Unknown retrofit ${fit as string}`); + } +} + +async function retrofitTypes(flags: Map) { + const version = flags.get('version'); + assertIsString(version); + + // ensure version exists + const tags = await getTags('ember-data-types'); + if (!tags.has(version)) { + throw new Error(`No published types exist for ${version}. You may want to try one of latest|beta|canary`); + } + + write(`🚀 Retrofitting types to use @${version} using the separate-type-package strategy`); + + // collect installed packages + const installed = new Map(); + const needed = new Map(); + const pkg = getPkgJson(); + const deps = pkg.dependencies ?? {}; + const devDeps = pkg.devDependencies ?? {}; + + Types.forEach((pkgName) => { + let found = false; + if (deps[pkgName]) { + found = true; + installed.set(pkgName, { version: deps[pkgName], isTypes: true, source: pkgNameFromTypes(pkgName) }); + } + if (devDeps[pkgName]) { + if (found) { + throw new Error( + `${pkgName} is currently in both .dependencies and .devDependencies. It should be removed from one of these.` + ); + } + installed.set(pkgName, { + dev: true, + version: devDeps[pkgName], + isTypes: true, + source: pkgNameFromTypes(pkgName), + }); + } + }); + + // for any main packages installed, we ensure we have the matching types + // package + Main.forEach((pkgName) => { + const typesPkg = getTypesPackageName(pkgName); + let found = false; + if (deps[pkgName]) { + found = true; + if (typesPkg && !installed.has(typesPkg)) { + needed.set(typesPkg, { version: deps[pkgName] }); + } + } + if (devDeps[pkgName]) { + if (found) { + throw new Error( + `${pkgName} is currently in both .dependencies and .devDependencies. It should be removed from one of these.` + ); + } + if (typesPkg && !installed.has(typesPkg)) { + needed.set(typesPkg, { dev: true, version: devDeps[pkgName], isTypes: true, source: pkgName }); + } + } + }); + + // if any mirror packages are installed, we recommend bumping them + // to match the same types version + // if (flags.get('mirror')) { + Mirror.forEach((pkgName) => { + let found = false; + if (deps[pkgName]) { + found = true; + installed.set(pkgName, { version: deps[pkgName] }); + } + if (devDeps[pkgName]) { + if (found) { + throw new Error( + `${pkgName} is currently in both .dependencies and .devDependencies. It should be removed from one of these.` + ); + } + installed.set(pkgName, { dev: true, version: devDeps[pkgName] }); + } + }); + // } + + // get matching version of each installed package + // from npm based on the dist-tag + const seen = new Set(); + const toInstall = new Map(); + + for (const [pkgName, available] of installed) { + seen.add(pkgName); + + const pkgInfo = await getInfo(`${pkgName}@${version}`); + const mainPkgInfo = available.isTypes ? await getInfo(`${available.source}@${available.version}`) : null; + if (!pkgInfo) { + throw new Error(`No published version for ${pkgName}@${version}`); + } + if (available.isTypes && !mainPkgInfo) { + throw new Error(`No published version for ${available.source}@${available.version}`); + } + + // if the version is the same + // we don't need to install it + if (available.version !== pkgInfo.version) { + toInstall.set(pkgName, { dev: available.dev, version: pkgInfo.version, existing: true }); + } + + // collect deps and peerDeps + const relatedInfo = mainPkgInfo || pkgInfo; + let relatedDeps = Object.assign({}, relatedInfo.dependencies, relatedInfo.peerDependencies); + + if (available.isTypes) { + const mainPkgDeps = relatedDeps; + relatedDeps = {}; + + for (const depName in mainPkgDeps) { + const typesPkg = getTypesPackageName(depName); + if (typesPkg) { + relatedDeps[typesPkg] = mainPkgDeps[depName]; + } + } + } + + for (const depName in relatedDeps) { + if (!ALL.includes(depName) || seen.has(depName) || installed.has(depName)) { + continue; + } + + seen.add(depName); + const depInfo = await getInfo(`${depName}@${relatedDeps[depName]}`); + if (!depInfo) { + throw new Error(`No published version for ${depName}@${relatedDeps[depName]}`); + } + + toInstall.set(depName, { dev: available.dev, version: depInfo.version }); + } + } + + // same for needed packages + for (const [pkgName, available] of needed) { + if (seen.has(pkgName)) { + continue; + } + + seen.add(pkgName); + + const pkgInfo = await getInfo(`${pkgName}@${version}`); + const mainPkgInfo = available.isTypes ? await getInfo(`${available.source}@${version}`) : null; + if (!pkgInfo) { + throw new Error(`No published version for ${pkgName}@${version}`); + } + if (available.isTypes && !mainPkgInfo) { + throw new Error(`No published version for ${available.source}@${available.version}`); + } + + toInstall.set(pkgName, { dev: available.dev, version: pkgInfo.version }); + + // collect deps and peerDeps + const relatedInfo = mainPkgInfo || pkgInfo; + let relatedDeps = Object.assign({}, relatedInfo.dependencies, relatedInfo.peerDependencies); + + if (available.isTypes) { + const mainPkgDeps = relatedDeps; + relatedDeps = {}; + + for (const depName in mainPkgDeps) { + const typesPkg = getTypesPackageName(depName); + if (typesPkg) { + relatedDeps[typesPkg] = mainPkgDeps[depName]; + } + } + } + + for (const depName in relatedDeps) { + if (!ALL.includes(depName) || seen.has(depName) || needed.has(depName)) { + continue; + } + + seen.add(depName); + const depInfo = await getInfo(`${depName}@${relatedDeps[depName]}`); + if (!depInfo) { + throw new Error(`No published version for ${depName}@${relatedDeps[depName]}`); + } + + toInstall.set(depName, { dev: available.dev, version: depInfo.version }); + } + } + + // add the packages to the package.json + // and install them + write(chalk.grey(`\t📦 Installing ${toInstall.size} packages`)); + if (toInstall.size > 0) { + // add the packages to the package.json + for (const [pkgName, config] of toInstall) { + if (config.dev) { + pkg.devDependencies = pkg.devDependencies ?? {}; + pkg.devDependencies[pkgName] = config.version; + } else { + pkg.dependencies = pkg.dependencies ?? {}; + pkg.dependencies[pkgName] = config.version; + } + } + + // resort the package.json + if (pkg.dependencies) { + const keys = Object.keys(pkg.dependencies ?? {}).sort(); + const sortedDeps: Record = {}; + for (const key of keys) { + sortedDeps[key] = pkg.dependencies[key]; + } + pkg.dependencies = sortedDeps; + } + if (pkg.devDependencies) { + const keys = Object.keys(pkg.devDependencies ?? {}).sort(); + const sortedDeps: Record = {}; + for (const key of keys) { + sortedDeps[key] = pkg.devDependencies[key]; + } + pkg.devDependencies = sortedDeps; + } + } + + const removed = new Set(); + for (const [pkgName] of DefinitelyTyped) { + if (deps[pkgName]) { + removed.add(pkgName); + delete deps[pkgName]; + } + if (devDeps[pkgName]) { + removed.add(pkgName); + delete devDeps[pkgName]; + } + } + write(chalk.grey(`\t🗑 Removing ${removed.size} DefinitelyTyped packages`)); + + if (removed.size > 0 || toInstall.size > 0) { + writePkgJson(pkg); + + // determine which package manager to use + // and install the packages + const pkgManager = getPackageManagerFromLockfile(); + const installCmd = `${pkgManager} install`; + await exec(installCmd); + } + + // ensure tsconfig for each installed and needed package + const fullTsConfigPath = path.join(process.cwd(), 'tsconfig.json'); + const hasTsConfig = fs.existsSync(fullTsConfigPath); + + if (!hasTsConfig) { + write(chalk.yellow(`\t⚠️ No tsconfig.json found in the current working directory`)); + const tsConfig = structuredClone(TS_CONFIG) as { compilerOptions: { types: string[] } }; + tsConfig.compilerOptions.types = ['ember-source/types']; + for (const [pkgName] of toInstall) { + if (Types.includes(pkgName)) { + const typePath = getTypePathFor(pkgName); + if (!typePath) { + throw new Error(`Could not find type path for ${pkgName}`); + } + + tsConfig.compilerOptions.types.push(`./node_modules/${pkgName}/${typePath}`); + } + } + tsConfig.compilerOptions.types.sort(); + fs.writeFileSync(fullTsConfigPath, JSON.stringify(tsConfig, null, 2) + '\n'); + write(chalk.grey(`\t✅ created a tsconfig.json`)); + } else { + let edited = false; + const tsConfig = JSONC.parse(fs.readFileSync(fullTsConfigPath, { encoding: 'utf-8' })) as { + compilerOptions?: { types?: string[] }; + }; + if (!tsConfig.compilerOptions) { + tsConfig.compilerOptions = { types: [] }; + } + if (!tsConfig.compilerOptions.types) { + tsConfig.compilerOptions.types = []; + } + + if (!tsConfig.compilerOptions.types.includes('ember-source/types')) { + edited = true; + tsConfig.compilerOptions.types.push('ember-source/types'); + } + + for (const [pkgName] of toInstall) { + if (Types.includes(pkgName)) { + const typePath = getTypePathFor(pkgName); + if (!typePath) { + throw new Error(`Could not find type path for ${pkgName}`); + } + + edited = true; + tsConfig.compilerOptions.types.push(`./node_modules/${pkgName}/${typePath}`); + } + } + + if (edited) { + tsConfig.compilerOptions.types.sort(); + fs.writeFileSync(fullTsConfigPath, JSONC.stringify(tsConfig, null, 2) + '\n'); + write(chalk.grey(`\t✅ updated tsconfig.json`)); + } + } +} + +function getTypesPackageName(pkgName: string) { + let typesPkgName: string; + + if (!pkgName.startsWith('@')) { + typesPkgName = pkgName + '-types'; + } else { + const parts = pkgName.split('/'); + parts[0] = parts[0] + '-types'; + typesPkgName = parts.join('/'); + } + + if (Types.includes(typesPkgName)) { + return typesPkgName; + } + + return null; +} + +function pkgNameFromTypes(pkgName: string) { + let mainPkgName: string; + if (!pkgName.startsWith('@')) { + mainPkgName = pkgName.slice(0, pkgName.length - '-types'.length); + } else { + const parts = pkgName.split('/'); + parts[0] = parts[0].slice(0, parts[0].length - '-types'.length); + mainPkgName = parts.join('/'); + } + + if (Main.includes(mainPkgName)) { + return mainPkgName; + } + + throw new Error(`Could not find main package for ${pkgName}`); +} diff --git a/packages/-warp-drive/src/warp-drive.ts b/packages/-warp-drive/src/warp-drive.ts index 61ed8931238..ad35bbef4f8 100644 --- a/packages/-warp-drive/src/warp-drive.ts +++ b/packages/-warp-drive/src/warp-drive.ts @@ -1,3 +1,3 @@ import { main } from './-private/warp-drive/main.ts'; -await main(); +void main(); diff --git a/packages/-warp-drive/vite.config.mjs b/packages/-warp-drive/vite.config.mjs index 370d3fc2cc7..eb06fade7b5 100644 --- a/packages/-warp-drive/vite.config.mjs +++ b/packages/-warp-drive/vite.config.mjs @@ -1,6 +1,6 @@ import { createConfig } from '@warp-drive/internal-config/vite/config.js'; -export const externals = []; +export const externals = ['node:child_process', 'fs', 'chalk', 'path', 'semver', 'comment-json']; export const entryPoints = ['./src/warp-drive.ts']; export default createConfig( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7294777f57..ee81f1dc31c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -324,6 +324,9 @@ importers: chalk: specifier: ^5.3.0 version: 5.3.0 + comment-json: + specifier: ^4.2.3 + version: 4.2.3 semver: specifier: ^7.6.2 version: 7.6.2 @@ -8296,7 +8299,6 @@ packages: /array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} - dev: true /array-to-error@1.1.1: resolution: {integrity: sha512-kqcQ8s7uQfg3UViYON3kCMcck3A9exxgq+riVuKy08Mx00VN4EJhK30L2VpjE58LQHKhcE/GRpvbVUhqTvqzGQ==} @@ -9932,7 +9934,6 @@ packages: esprima: 4.0.1 has-own-prop: 2.0.0 repeat-string: 1.6.1 - dev: true /common-ancestor-path@1.0.1: resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} @@ -13402,7 +13403,6 @@ packages: /has-own-prop@2.0.0: resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} engines: {node: '>=8'} - dev: true /has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} diff --git a/release/strategy.json b/release/strategy.json index 7b17bdd44d6..c6d799fceca 100644 --- a/release/strategy.json +++ b/release/strategy.json @@ -41,6 +41,18 @@ "types": "private", "typesPublish": false }, + "warp-drive": { + "stage": "alpha", + "types": "private", + "typesPublish": false, + "mirrorPublish": false + }, + "@warp-drive/build-config": { + "stage": "alpha", + "types": "alpha", + "typesPublish": false, + "mirrorPublish": true + }, "@warp-drive/holodeck": { "stage": "alpha", "types": "private",