mirror of
https://github.com/enso-org/enso.git
synced 2024-11-26 08:52:58 +03:00
Fix importing .enso-project
files (#10379)
- Fix #10282 # Important Notes None
This commit is contained in:
parent
dc7aa94348
commit
19d5bdb9da
@ -34,7 +34,7 @@ const DEFAULT_IMPORT_ONLY_MODULES =
|
||||
'@vitejs\\u002Fplugin-react|node:process|chalk|string-length|yargs|yargs\\u002Fyargs|sharp|to-ico|connect|morgan|serve-static|tiny-invariant|clsx|create-servers|electron-is-dev|fast-glob|esbuild-plugin-.+|opener|tailwindcss.*|enso-assets.*|@modyfi\\u002Fvite-plugin-yaml|is-network-error|validator.+|.*[.]json$'
|
||||
const OUR_MODULES = 'enso-.*'
|
||||
const RELATIVE_MODULES =
|
||||
'bin\\u002Fproject-manager|bin\\u002Fserver|config\\u002Fparser|authentication|config|debug|detect|file-associations|index|ipc|log|naming|paths|preload|project-management|security|url-associations|#\\u002F.*'
|
||||
'bin\\u002Fproject-manager|bin\\u002Fserver|config\\u002Fparser|authentication|config|debug|desktop-environment|detect|file-associations|index|ipc|log|naming|paths|preload|project-management|security|url-associations|#\\u002F.*'
|
||||
const ALLOWED_DEFAULT_IMPORT_MODULES = `${DEFAULT_IMPORT_ONLY_MODULES}|postcss|ajv\\u002Fdist\\u002F2020|${RELATIVE_MODULES}`
|
||||
const STRING_LITERAL = ':matches(Literal[raw=/^["\']/], TemplateLiteral)'
|
||||
const NOT_CAMEL_CASE = '/^(?!_?[a-z][a-z0-9*]*([A-Z0-9][a-z0-9]*)*$)(?!React$)/'
|
||||
|
@ -16,7 +16,7 @@ import * as common from 'enso-common'
|
||||
import GLOBAL_CONFIG from 'enso-common/src/config.json' assert { type: 'json' }
|
||||
import * as contentConfig from 'enso-content-config'
|
||||
import * as ydocServer from 'enso-gui2/ydoc-server'
|
||||
import * as projectManagement from 'enso-project-manager-shim/src/projectManagement'
|
||||
import * as projectManagement from 'project-management'
|
||||
|
||||
import * as paths from '../paths'
|
||||
|
||||
|
81
app/ide-desktop/lib/client/src/desktop-environment.ts
Normal file
81
app/ide-desktop/lib/client/src/desktop-environment.ts
Normal file
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @file This module contains the logic for the detection of user-specific desktop environment attributes.
|
||||
*/
|
||||
|
||||
import * as childProcess from 'node:child_process'
|
||||
import * as os from 'node:os'
|
||||
import * as path from 'node:path'
|
||||
|
||||
export const DOCUMENTS = getDocumentsPath()
|
||||
|
||||
const CHILD_PROCESS_TIMEOUT = 3000
|
||||
|
||||
/**
|
||||
* Detects path of the user documents directory depending on the operating system.
|
||||
*/
|
||||
function getDocumentsPath(): string | undefined {
|
||||
if (process.platform === 'linux') {
|
||||
return getLinuxDocumentsPath()
|
||||
} else if (process.platform === 'darwin') {
|
||||
return getMacOsDocumentsPath()
|
||||
} else if (process.platform === 'win32') {
|
||||
return getWindowsDocumentsPath()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user documents path on Linux.
|
||||
*/
|
||||
function getLinuxDocumentsPath(): string {
|
||||
const xdgDocumentsPath = getXdgDocumentsPath()
|
||||
|
||||
return xdgDocumentsPath ?? path.join(os.homedir(), 'enso')
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the documents directory from the XDG directory management system.
|
||||
*/
|
||||
function getXdgDocumentsPath(): string | undefined {
|
||||
const out = childProcess.spawnSync('xdg-user-dir', ['DOCUMENTS'], {
|
||||
timeout: CHILD_PROCESS_TIMEOUT,
|
||||
})
|
||||
|
||||
if (out.error !== undefined) {
|
||||
return
|
||||
} else {
|
||||
return out.stdout.toString().trim()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user documents path. On macOS, `Documents` acts as a symlink pointing to the
|
||||
* real locale-specific user documents directory.
|
||||
*/
|
||||
function getMacOsDocumentsPath(): string {
|
||||
return path.join(os.homedir(), 'Documents')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the `My Documents` Windows directory.
|
||||
*/
|
||||
function getWindowsDocumentsPath(): string | undefined {
|
||||
const out = childProcess.spawnSync(
|
||||
'reg',
|
||||
[
|
||||
'query',
|
||||
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders',
|
||||
'/v',
|
||||
'personal',
|
||||
],
|
||||
{ timeout: CHILD_PROCESS_TIMEOUT }
|
||||
)
|
||||
|
||||
if (out.error !== undefined) {
|
||||
return
|
||||
} else {
|
||||
const stdoutString = out.stdout.toString()
|
||||
return stdoutString.split(/\s\s+/)[4]
|
||||
}
|
||||
}
|
@ -7,23 +7,28 @@
|
||||
* - if the project is in a different location, we copy it to the Project Manager's location
|
||||
* and open it.
|
||||
* - if the project is a bundle, we extract it to the Project Manager's location and open it. */
|
||||
|
||||
import * as crypto from 'node:crypto'
|
||||
import * as fs from 'node:fs'
|
||||
import * as os from 'node:os'
|
||||
import * as pathModule from 'node:path'
|
||||
import type * as stream from 'node:stream'
|
||||
|
||||
import * as electron from 'electron'
|
||||
import * as tar from 'tar'
|
||||
|
||||
import * as common from 'enso-common'
|
||||
import * as buildUtils from 'enso-common/src/buildUtils'
|
||||
import * as config from 'enso-content-config'
|
||||
import * as desktopEnvironment from 'desktop-environment'
|
||||
|
||||
import * as paths from 'paths'
|
||||
import * as fileAssociations from '../file-associations'
|
||||
const logger = console
|
||||
|
||||
const logger = config.logger
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
export const PACKAGE_METADATA_RELATIVE_PATH = 'package.yaml'
|
||||
export const PROJECT_METADATA_RELATIVE_PATH = '.enso/project.json'
|
||||
/** The filename suffix for the project bundle, including the leading period character. */
|
||||
const BUNDLED_PROJECT_SUFFIX = '.enso-project'
|
||||
|
||||
// ======================
|
||||
// === Project Import ===
|
||||
@ -40,7 +45,7 @@ export function importProjectFromPath(
|
||||
name: string | null = null
|
||||
): string {
|
||||
directory ??= getProjectsDirectory()
|
||||
if (pathModule.extname(openedPath).endsWith(fileAssociations.BUNDLED_PROJECT_SUFFIX)) {
|
||||
if (pathModule.extname(openedPath).endsWith(BUNDLED_PROJECT_SUFFIX)) {
|
||||
logger.log(`Path '${openedPath}' denotes a bundled project.`)
|
||||
// The second part of condition is for the case when someone names a directory
|
||||
// like `my-project.enso-project` and stores the project there.
|
||||
@ -52,7 +57,7 @@ export function importProjectFromPath(
|
||||
return importBundle(openedPath, directory, name)
|
||||
}
|
||||
} else {
|
||||
logger.log(`Opening non-bundled file '${openedPath}'.`)
|
||||
logger.log(`Opening non-bundled file: '${openedPath}'.`)
|
||||
const rootPath = getProjectRoot(openedPath)
|
||||
// Check if the project root is under the projects directory. If it is, we can open it.
|
||||
// Otherwise, we need to install it first.
|
||||
@ -123,7 +128,7 @@ export function importBundle(
|
||||
sync: true,
|
||||
strip: rootPieces.length,
|
||||
})
|
||||
return bumpMetadata(targetPath, name ?? null)
|
||||
return bumpMetadata(targetPath, directory, name ?? null)
|
||||
}
|
||||
|
||||
/** Upload the project from a bundle. */
|
||||
@ -152,7 +157,7 @@ export async function uploadBundle(
|
||||
fs.rmdirSync(temporaryDirectoryName)
|
||||
}
|
||||
}
|
||||
return bumpMetadata(targetPath, name ?? null)
|
||||
return bumpMetadata(targetPath, directory, name ?? null)
|
||||
}
|
||||
|
||||
/** Import the project so it becomes visible to the Project Manager.
|
||||
@ -185,7 +190,7 @@ export function importDirectory(
|
||||
fs.cpSync(rootPath, targetPath, { recursive: true })
|
||||
// Update the project ID, so we are certain that it is unique.
|
||||
// This would be violated, if we imported the same project multiple times.
|
||||
return bumpMetadata(targetPath, name ?? null)
|
||||
return bumpMetadata(targetPath, directory, name ?? null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -226,9 +231,17 @@ export function getProjectId(projectRoot: string): string | null {
|
||||
return getMetadata(projectRoot)?.id ?? null
|
||||
}
|
||||
|
||||
/** Get the package name. */
|
||||
function getPackageName(projectRoot: string) {
|
||||
const path = pathModule.join(projectRoot, PACKAGE_METADATA_RELATIVE_PATH)
|
||||
const contents = fs.readFileSync(path, { encoding: 'utf-8' })
|
||||
const [, name] = contents.match(/^name: (.*)/) ?? []
|
||||
return name ?? null
|
||||
}
|
||||
|
||||
/** Update the package name. */
|
||||
export function updatePackageName(projectRoot: string, name: string) {
|
||||
const path = pathModule.join(projectRoot, paths.PACKAGE_METADATA_RELATIVE)
|
||||
const path = pathModule.join(projectRoot, PACKAGE_METADATA_RELATIVE_PATH)
|
||||
const contents = fs.readFileSync(path, { encoding: 'utf-8' })
|
||||
const newContents = contents.replace(/^name: .*/, `name: ${name}`)
|
||||
fs.writeFileSync(path, newContents)
|
||||
@ -246,7 +259,7 @@ export function createMetadata(): ProjectMetadata {
|
||||
|
||||
/** Retrieve the project's metadata. */
|
||||
export function getMetadata(projectRoot: string): ProjectMetadata | null {
|
||||
const metadataPath = pathModule.join(projectRoot, paths.PROJECT_METADATA_RELATIVE)
|
||||
const metadataPath = pathModule.join(projectRoot, PROJECT_METADATA_RELATIVE_PATH)
|
||||
try {
|
||||
const jsonText = fs.readFileSync(metadataPath, 'utf8')
|
||||
const metadata: unknown = JSON.parse(jsonText)
|
||||
@ -258,7 +271,7 @@ export function getMetadata(projectRoot: string): ProjectMetadata | null {
|
||||
|
||||
/** Write the project's metadata. */
|
||||
export function writeMetadata(projectRoot: string, metadata: ProjectMetadata): void {
|
||||
const metadataPath = pathModule.join(projectRoot, paths.PROJECT_METADATA_RELATIVE)
|
||||
const metadataPath = pathModule.join(projectRoot, PROJECT_METADATA_RELATIVE_PATH)
|
||||
fs.mkdirSync(pathModule.dirname(metadataPath), { recursive: true })
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, buildUtils.INDENT_SIZE))
|
||||
}
|
||||
@ -284,7 +297,7 @@ export function updateMetadata(
|
||||
/** Check if the given path represents the root of an Enso project.
|
||||
* This is decided by the presence of the Project Manager's metadata. */
|
||||
export function isProjectRoot(candidatePath: string): boolean {
|
||||
const projectJsonPath = pathModule.join(candidatePath, paths.PROJECT_METADATA_RELATIVE)
|
||||
const projectJsonPath = pathModule.join(candidatePath, PROJECT_METADATA_RELATIVE_PATH)
|
||||
try {
|
||||
fs.accessSync(projectJsonPath, fs.constants.R_OK)
|
||||
return true
|
||||
@ -365,7 +378,12 @@ export function getProjectRoot(subtreePath: string): string | null {
|
||||
|
||||
/** Get the directory that stores Enso projects. */
|
||||
export function getProjectsDirectory(): string {
|
||||
return pathModule.join(electron.app.getPath('home'), 'enso', 'projects')
|
||||
const documentsPath = desktopEnvironment.DOCUMENTS
|
||||
if (documentsPath === undefined) {
|
||||
return pathModule.join(os.homedir(), 'enso', 'projects')
|
||||
} else {
|
||||
return pathModule.join(documentsPath, 'enso-projects')
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if the given project is installed, i.e. can be opened with the Project Manager. */
|
||||
@ -387,13 +405,38 @@ export function generateId(): string {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
|
||||
/** Update the project's ID to a new, unique value, and its last opened date to the current date.
|
||||
* Return the new ID. */
|
||||
export function bumpMetadata(projectRoot: string, name: string | null): string {
|
||||
if (name != null) {
|
||||
console.log('nom', name)
|
||||
updatePackageName(projectRoot, name)
|
||||
/** Update the project's ID to a new, unique value, and its last opened date to the current date. */
|
||||
export function bumpMetadata(
|
||||
projectRoot: string,
|
||||
parentDirectory: string,
|
||||
name: string | null
|
||||
): string {
|
||||
if (name == null) {
|
||||
const currentName = getPackageName(projectRoot) ?? ''
|
||||
let index: number | null = null
|
||||
const prefix = `${currentName} `
|
||||
for (const sibling of fs.readdirSync(parentDirectory, { withFileTypes: true })) {
|
||||
if (sibling.isDirectory()) {
|
||||
try {
|
||||
const siblingPath = pathModule.join(parentDirectory, sibling.name)
|
||||
const siblingName = getPackageName(siblingPath)
|
||||
if (siblingName === currentName) {
|
||||
index = index ?? 2
|
||||
} else if (siblingName != null && siblingName.startsWith(prefix)) {
|
||||
const suffix = siblingName.replace(prefix, '')
|
||||
const [, numberString] = suffix.match(/^\((\d+)\)/) ?? []
|
||||
if (numberString != null) {
|
||||
index = Math.max(index ?? 2, Number(numberString) + 1)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignored - it is a directory but not a project.
|
||||
}
|
||||
}
|
||||
}
|
||||
name = index == null ? currentName : `${currentName} (${index})`
|
||||
}
|
||||
updatePackageName(projectRoot, name)
|
||||
return updateMetadata(projectRoot, metadata => ({
|
||||
...metadata,
|
||||
id: generateId(),
|
||||
|
@ -2,6 +2,8 @@
|
||||
* interactive components. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as validator from 'validator'
|
||||
|
||||
import DriveIcon from 'enso-assets/drive.svg'
|
||||
import EditorIcon from 'enso-assets/network.svg'
|
||||
import SettingsIcon from 'enso-assets/settings.svg'
|
||||
@ -35,7 +37,8 @@ import UserBar from '#/layouts/UserBar'
|
||||
import Page from '#/components/Page'
|
||||
|
||||
import * as backendModule from '#/services/Backend'
|
||||
import type * as projectManager from '#/services/ProjectManager'
|
||||
import * as localBackendModule from '#/services/LocalBackend'
|
||||
import * as projectManager from '#/services/ProjectManager'
|
||||
|
||||
import * as array from '#/utilities/array'
|
||||
import LocalStorage from '#/utilities/LocalStorage'
|
||||
@ -110,15 +113,12 @@ export interface DashboardProps {
|
||||
readonly supportsLocalBackend: boolean
|
||||
readonly appRunner: types.EditorRunner | null
|
||||
readonly initialProjectName: string | null
|
||||
readonly projectManagerUrl: string | null
|
||||
readonly ydocUrl: string | null
|
||||
readonly projectManagerRootDirectory: projectManager.Path | null
|
||||
}
|
||||
|
||||
/** The component that contains the entire UI. */
|
||||
export default function Dashboard(props: DashboardProps) {
|
||||
const { appRunner, initialProjectName } = props
|
||||
const { ydocUrl, projectManagerUrl, projectManagerRootDirectory } = props
|
||||
const { appRunner, ydocUrl, initialProjectName: initialProjectNameRaw } = props
|
||||
const session = authProvider.useNonPartialUserSession()
|
||||
const remoteBackend = backendProvider.useRemoteBackend()
|
||||
const localBackend = backendProvider.useLocalBackend()
|
||||
@ -144,7 +144,13 @@ export default function Dashboard(props: DashboardProps) {
|
||||
const [assetListEvents, dispatchAssetListEvent] =
|
||||
eventHooks.useEvent<assetListEvent.AssetListEvent>()
|
||||
const [assetEvents, dispatchAssetEvent] = eventHooks.useEvent<assetEvent.AssetEvent>()
|
||||
const defaultCategory = remoteBackend != null ? Category.cloud : Category.local
|
||||
const initialLocalProjectId =
|
||||
initialProjectNameRaw != null && validator.isUUID(initialProjectNameRaw)
|
||||
? localBackendModule.newProjectId(projectManager.UUID(initialProjectNameRaw))
|
||||
: null
|
||||
const initialProjectName = initialLocalProjectId ?? initialProjectNameRaw
|
||||
const defaultCategory =
|
||||
remoteBackend != null && initialLocalProjectId == null ? Category.cloud : Category.local
|
||||
const [category, setCategory] = searchParamsState.useSearchParamsState(
|
||||
'driveCategory',
|
||||
() => defaultCategory,
|
||||
@ -178,57 +184,66 @@ export default function Dashboard(props: DashboardProps) {
|
||||
setPage(TabType.drive)
|
||||
}
|
||||
} else if (savedProjectStartupInfo != null) {
|
||||
if (savedProjectStartupInfo.backendType === backendModule.BackendType.remote) {
|
||||
if (remoteBackend != null) {
|
||||
setPage(TabType.drive)
|
||||
void (async () => {
|
||||
const abortController = new AbortController()
|
||||
openProjectAbortControllerRef.current = abortController
|
||||
try {
|
||||
const oldProject = await remoteBackend.getProjectDetails(
|
||||
savedProjectStartupInfo.projectAsset.id,
|
||||
savedProjectStartupInfo.projectAsset.parentId,
|
||||
savedProjectStartupInfo.projectAsset.title
|
||||
)
|
||||
if (backendModule.IS_OPENING_OR_OPENED[oldProject.state.type]) {
|
||||
const project = remoteBackend.waitUntilProjectIsReady(
|
||||
switch (savedProjectStartupInfo.backendType) {
|
||||
case backendModule.BackendType.remote: {
|
||||
if (remoteBackend != null) {
|
||||
setPage(TabType.drive)
|
||||
void (async () => {
|
||||
const abortController = new AbortController()
|
||||
openProjectAbortControllerRef.current = abortController
|
||||
try {
|
||||
const oldProject = await remoteBackend.getProjectDetails(
|
||||
savedProjectStartupInfo.projectAsset.id,
|
||||
savedProjectStartupInfo.projectAsset.parentId,
|
||||
savedProjectStartupInfo.projectAsset.title,
|
||||
abortController.signal
|
||||
savedProjectStartupInfo.projectAsset.title
|
||||
)
|
||||
setProjectStartupInfo({ ...savedProjectStartupInfo, project })
|
||||
if (page === TabType.editor) {
|
||||
setPage(page)
|
||||
if (backendModule.IS_OPENING_OR_OPENED[oldProject.state.type]) {
|
||||
const project = remoteBackend.waitUntilProjectIsReady(
|
||||
savedProjectStartupInfo.projectAsset.id,
|
||||
savedProjectStartupInfo.projectAsset.parentId,
|
||||
savedProjectStartupInfo.projectAsset.title,
|
||||
abortController.signal
|
||||
)
|
||||
setProjectStartupInfo({ ...savedProjectStartupInfo, project })
|
||||
if (page === TabType.editor) {
|
||||
setPage(page)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setProjectStartupInfo(null)
|
||||
}
|
||||
} catch {
|
||||
setProjectStartupInfo(null)
|
||||
}
|
||||
})()
|
||||
})()
|
||||
}
|
||||
break
|
||||
}
|
||||
} else if (projectManagerUrl != null && projectManagerRootDirectory != null) {
|
||||
if (localBackend != null) {
|
||||
void (async () => {
|
||||
await localBackend.openProject(
|
||||
savedProjectStartupInfo.projectAsset.id,
|
||||
{
|
||||
executeAsync: false,
|
||||
cognitoCredentials: null,
|
||||
parentId: savedProjectStartupInfo.projectAsset.parentId,
|
||||
},
|
||||
savedProjectStartupInfo.projectAsset.title
|
||||
)
|
||||
const project = localBackend.getProjectDetails(
|
||||
savedProjectStartupInfo.projectAsset.id,
|
||||
savedProjectStartupInfo.projectAsset.parentId,
|
||||
savedProjectStartupInfo.projectAsset.title
|
||||
)
|
||||
case backendModule.BackendType.local: {
|
||||
if (localBackend != null) {
|
||||
const project = localBackend
|
||||
.openProject(
|
||||
savedProjectStartupInfo.projectAsset.id,
|
||||
{
|
||||
executeAsync: false,
|
||||
cognitoCredentials: null,
|
||||
parentId: savedProjectStartupInfo.projectAsset.parentId,
|
||||
},
|
||||
savedProjectStartupInfo.projectAsset.title
|
||||
)
|
||||
.then(() =>
|
||||
localBackend.getProjectDetails(
|
||||
savedProjectStartupInfo.projectAsset.id,
|
||||
savedProjectStartupInfo.projectAsset.parentId,
|
||||
savedProjectStartupInfo.projectAsset.title
|
||||
)
|
||||
)
|
||||
.catch(error => {
|
||||
setProjectStartupInfo(null)
|
||||
throw error
|
||||
})
|
||||
setProjectStartupInfo({ ...savedProjectStartupInfo, project })
|
||||
if (page === TabType.editor) {
|
||||
setPage(page)
|
||||
}
|
||||
})()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ const logger = console
|
||||
export const PACKAGE_METADATA_RELATIVE_PATH = 'package.yaml'
|
||||
export const PROJECT_METADATA_RELATIVE_PATH = '.enso/project.json'
|
||||
/** The filename suffix for the project bundle, including the leading period character. */
|
||||
const BUNDLED_PROJECT_SUFFIX = `.enso-project`
|
||||
const BUNDLED_PROJECT_SUFFIX = '.enso-project'
|
||||
|
||||
// ======================
|
||||
// === Project Import ===
|
||||
@ -128,7 +128,7 @@ export function importBundle(
|
||||
sync: true,
|
||||
strip: rootPieces.length,
|
||||
})
|
||||
return bumpMetadata(targetPath, name ?? null)
|
||||
return bumpMetadata(targetPath, directory, name ?? null)
|
||||
}
|
||||
|
||||
/** Upload the project from a bundle. */
|
||||
@ -157,7 +157,7 @@ export async function uploadBundle(
|
||||
fs.rmdirSync(temporaryDirectoryName)
|
||||
}
|
||||
}
|
||||
return bumpMetadata(targetPath, name ?? null)
|
||||
return bumpMetadata(targetPath, directory, name ?? null)
|
||||
}
|
||||
|
||||
/** Import the project so it becomes visible to the Project Manager.
|
||||
@ -190,7 +190,7 @@ export function importDirectory(
|
||||
fs.cpSync(rootPath, targetPath, { recursive: true })
|
||||
// Update the project ID, so we are certain that it is unique.
|
||||
// This would be violated, if we imported the same project multiple times.
|
||||
return bumpMetadata(targetPath, name ?? null)
|
||||
return bumpMetadata(targetPath, directory, name ?? null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -231,6 +231,14 @@ export function getProjectId(projectRoot: string): string | null {
|
||||
return getMetadata(projectRoot)?.id ?? null
|
||||
}
|
||||
|
||||
/** Get the package name. */
|
||||
function getPackageName(projectRoot: string) {
|
||||
const path = pathModule.join(projectRoot, PACKAGE_METADATA_RELATIVE_PATH)
|
||||
const contents = fs.readFileSync(path, { encoding: 'utf-8' })
|
||||
const [, name] = contents.match(/^name: (.*)/) ?? []
|
||||
return name ?? null
|
||||
}
|
||||
|
||||
/** Update the package name. */
|
||||
export function updatePackageName(projectRoot: string, name: string) {
|
||||
const path = pathModule.join(projectRoot, PACKAGE_METADATA_RELATIVE_PATH)
|
||||
@ -398,10 +406,37 @@ export function generateId(): string {
|
||||
}
|
||||
|
||||
/** Update the project's ID to a new, unique value, and its last opened date to the current date. */
|
||||
export function bumpMetadata(projectRoot: string, name: string | null): string {
|
||||
if (name != null) {
|
||||
updatePackageName(projectRoot, name)
|
||||
export function bumpMetadata(
|
||||
projectRoot: string,
|
||||
parentDirectory: string,
|
||||
name: string | null
|
||||
): string {
|
||||
if (name == null) {
|
||||
const currentName = getPackageName(projectRoot) ?? ''
|
||||
let index: number | null = null
|
||||
const prefix = `${currentName} `
|
||||
for (const sibling of fs.readdirSync(parentDirectory, { withFileTypes: true })) {
|
||||
if (sibling.isDirectory()) {
|
||||
try {
|
||||
const siblingPath = pathModule.join(parentDirectory, sibling.name)
|
||||
const siblingName = getPackageName(siblingPath)
|
||||
if (siblingName === currentName) {
|
||||
index = index ?? 2
|
||||
} else if (siblingName != null && siblingName.startsWith(prefix)) {
|
||||
const suffix = siblingName.replace(prefix, '')
|
||||
const [, numberString] = suffix.match(/^\((\d+)\)/) ?? []
|
||||
if (numberString != null) {
|
||||
index = Math.max(index ?? 2, Number(numberString) + 1)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignored - it is a directory but not a project.
|
||||
}
|
||||
}
|
||||
}
|
||||
name = index == null ? currentName : `${currentName} (${index})`
|
||||
}
|
||||
updatePackageName(projectRoot, name)
|
||||
return updateMetadata(projectRoot, metadata => ({
|
||||
...metadata,
|
||||
id: generateId(),
|
||||
|
Loading…
Reference in New Issue
Block a user