mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-01 06:22:47 +02:00
#653 - Read Astro Content Collections
This commit is contained in:
@@ -23,6 +23,13 @@
|
||||
"common.filter.value": "Filter by {0}",
|
||||
"common.error.message": "Sorry, something went wrong.",
|
||||
"common.openOnWebsite": "Open on website",
|
||||
"common.settings": "Settings",
|
||||
|
||||
"settings.contentTypes": "Content types",
|
||||
"settings.contentFolders": "Content folders",
|
||||
"settings.diagnostic": "Diagnostic",
|
||||
"settings.diagnostic.description": "You can run the diagnostics to check the whole Front Matter CMS configuration.",
|
||||
"settings.diagnostic.link": "Run full diagnostics",
|
||||
|
||||
"developer.title": "Developer mode",
|
||||
"developer.reload.title": "Reload the dashboard",
|
||||
@@ -246,6 +253,7 @@
|
||||
"dashboard.steps.stepsToGetStarted.showDashboard.description": "Once all actions are completed, the dashboard can be loaded.",
|
||||
"dashboard.steps.stepsToGetStarted.template.name": "Use a configuration template",
|
||||
"dashboard.steps.stepsToGetStarted.template.description": "Select a template to prefill the frontmatter.json file with the recommended settings.",
|
||||
"dashboard.steps.stepsToGetStarted.astroContentTypes.name": "Create Content-Types for your Astro Content Collections",
|
||||
|
||||
"dashboard.taxonomyView.button.add.title": "Add {0} to taxonomy settings",
|
||||
"dashboard.taxonomyView.button.edit.title": "Edit {0}",
|
||||
@@ -286,6 +294,9 @@
|
||||
|
||||
"dashboard.media.detailsSlideOver.unmapped.description": "Do you want to remap the metadata of unmapped files?",
|
||||
|
||||
"dashboard.configuration.astro.astroContentTypes.empty": "No Astro Content Collections found.",
|
||||
"dashboard.configuration.astro.astroContentTypes.description": "The following Astro Content Collections and can be used to generate a content-type.",
|
||||
|
||||
"panel.contentType.contentTypeValidator.title": "Content-type",
|
||||
"panel.contentType.contentTypeValidator.hint": "We noticed field differences between the content-type and the front matter data. \n Would you like to create, update, or set the content-type for this content?",
|
||||
"panel.contentType.contentTypeValidator.button.create": "Create content-type",
|
||||
|
||||
@@ -30,7 +30,7 @@ const glob = require('glob');
|
||||
for (const key of enKeys) {
|
||||
// If the key does not exist in the file, add it
|
||||
if (!content[key]) {
|
||||
content[key] = `🚧: ${enContent[key]}`;
|
||||
content[key] = `${enContent[key]}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
SETTING_EXTENSIBILITY_SCRIPTS
|
||||
} from '../constants';
|
||||
import { join } from 'path';
|
||||
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window, workspace } from 'vscode';
|
||||
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window } from 'vscode';
|
||||
import { Logger, Settings as SettingsHelper } from '../helpers';
|
||||
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
|
||||
import { Extension } from '../helpers/Extension';
|
||||
@@ -23,7 +23,8 @@ import {
|
||||
SnippetListener,
|
||||
TaxonomyListener,
|
||||
LogListener,
|
||||
LocalizationListener
|
||||
LocalizationListener,
|
||||
SsgListener
|
||||
} from '../listeners/dashboard';
|
||||
import { MediaListener as PanelMediaListener } from '../listeners/panel';
|
||||
import { GitListener, ModeListener } from '../listeners/general';
|
||||
@@ -180,6 +181,7 @@ export class Dashboard {
|
||||
GitListener.process(msg);
|
||||
TaxonomyListener.process(msg);
|
||||
LogListener.process(msg);
|
||||
SsgListener.process(msg);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+12
-3
@@ -109,9 +109,12 @@ export class Folders {
|
||||
* Register the new folder path
|
||||
* @param folderInfo
|
||||
*/
|
||||
public static async register(folderInfo: { title: string; path: Uri } | Uri) {
|
||||
public static async register(
|
||||
folderInfo: { title: string; path: Uri; contentType: string[] } | Uri
|
||||
) {
|
||||
let folderName = folderInfo instanceof Uri ? undefined : folderInfo.title;
|
||||
const folder = folderInfo instanceof Uri ? folderInfo : folderInfo.path;
|
||||
const contentType = folderInfo instanceof Uri ? undefined : folderInfo.contentType;
|
||||
|
||||
if (folder && folder.fsPath) {
|
||||
const wslPath = folder.fsPath.replace(/\//g, '\\');
|
||||
@@ -137,10 +140,16 @@ export class Folders {
|
||||
});
|
||||
}
|
||||
|
||||
folders.push({
|
||||
const contentFolder = {
|
||||
title: folderName,
|
||||
path: folder.fsPath
|
||||
} as ContentFolder);
|
||||
} as ContentFolder;
|
||||
|
||||
if (contentType) {
|
||||
contentFolder.contentTypes = typeof contentType === 'string' ? [contentType] : contentType;
|
||||
}
|
||||
|
||||
folders.push(contentFolder);
|
||||
|
||||
folders = uniqBy(folders, (f) => f.path);
|
||||
await Folders.update(folders);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export const SsgScripts = {
|
||||
folder: '/ssg-scripts',
|
||||
astroContentCollection: 'astro-collections.mjs'
|
||||
};
|
||||
@@ -14,6 +14,7 @@ export * from './NotificationType';
|
||||
export * from './PreviewCommands';
|
||||
export * from './SentryIgnore';
|
||||
export * from './Snippet';
|
||||
export * from './SsgScripts';
|
||||
export * from './StaticFolderPlaceholder';
|
||||
export * from './TelemetryEvent';
|
||||
export * from './Templates';
|
||||
|
||||
@@ -14,6 +14,8 @@ export enum DashboardMessage {
|
||||
addFolder = 'addFolder',
|
||||
addAssetsFolder = 'addAssetsFolder',
|
||||
triggerTemplate = 'triggerTemplate',
|
||||
ssgGetAstroContentTypes = 'ssgGetAstroContentTypes',
|
||||
ssgSetAstroContentTypes = 'ssgSetAstroContentTypes',
|
||||
|
||||
// Content dashboard
|
||||
getData = 'getData',
|
||||
|
||||
@@ -21,6 +21,7 @@ import { ErrorView } from './ErrorView';
|
||||
import { DashboardMessage } from '../DashboardMessage';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../localization';
|
||||
import { SettingsView } from './SettingsView/SettingsView';
|
||||
|
||||
export interface IAppProps {
|
||||
showWelcome: boolean;
|
||||
@@ -133,6 +134,8 @@ Stack: ${componentStack}`
|
||||
<Route path={routePaths.taxonomy} element={<TaxonomyView pages={pages} />} />
|
||||
)}
|
||||
|
||||
<Route path={routePaths.settings} element={<SettingsView />} />
|
||||
|
||||
<Route path={`*`} element={<UnknownView />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import * as React from 'react';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { messageHandler } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../../../DashboardMessage';
|
||||
import { AstroCollection } from '../../../../models';
|
||||
import { Settings } from '../../../models';
|
||||
import { SelectItem } from '../../Steps/SelectItem';
|
||||
import { LocalizationKey } from '../../../../localization';
|
||||
|
||||
export interface IAstroContentTypesProps {
|
||||
settings: Settings
|
||||
triggerLoading: (isLoading: boolean) => void;
|
||||
}
|
||||
|
||||
export const AstroContentTypes: React.FunctionComponent<IAstroContentTypesProps> = ({
|
||||
settings,
|
||||
triggerLoading
|
||||
}: React.PropsWithChildren<IAstroContentTypesProps>) => {
|
||||
const [collections, setCollections] = React.useState<AstroCollection[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
triggerLoading(true);
|
||||
messageHandler.request<AstroCollection[]>(DashboardMessage.ssgGetAstroContentTypes).then((result) => {
|
||||
triggerLoading(false);
|
||||
setCollections(result);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const generateContentType = (collection: AstroCollection) => {
|
||||
triggerLoading(true);
|
||||
messageHandler.request(DashboardMessage.ssgSetAstroContentTypes, {
|
||||
collection
|
||||
}).then((result) => {
|
||||
triggerLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (!collections || collections.length === 0) {
|
||||
return (
|
||||
<div className='mt-1'>
|
||||
{l10n.t(LocalizationKey.dashboardConfigurationAstroAstroContentTypesEmpty)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-1'>
|
||||
<p>{l10n.t(LocalizationKey.dashboardConfigurationAstroAstroContentTypesDescription)}</p>
|
||||
|
||||
<div className='mt-2'>
|
||||
{
|
||||
collections.map((collection) => {
|
||||
const ct = settings.contentTypes.find((c) => c.name === collection.name);
|
||||
|
||||
return (
|
||||
<SelectItem
|
||||
key={collection.name}
|
||||
title={collection.name}
|
||||
buttonTitle={collection.name}
|
||||
isSelected={ct !== undefined}
|
||||
onClick={() => generateContentType(collection)}
|
||||
disabled={ct !== undefined} />
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../../localization';
|
||||
import { Settings } from '../../../models';
|
||||
import { Folder } from './Folder';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../../../DashboardMessage';
|
||||
|
||||
export interface IContentFoldersProps {
|
||||
settings: Settings
|
||||
triggerLoading: (isLoading: boolean) => void;
|
||||
}
|
||||
|
||||
export const ContentFolders: React.FunctionComponent<IContentFoldersProps> = ({
|
||||
settings
|
||||
}: React.PropsWithChildren<IContentFoldersProps>) => {
|
||||
|
||||
const addFolder = (folder: string) => {
|
||||
Messenger.send(DashboardMessage.addFolder, folder);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
{l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedContentFoldersDescription)}
|
||||
</p>
|
||||
|
||||
{settings?.dashboardState?.welcome?.contentFolders?.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<div className="text-sm">{l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedContentFoldersLabel)}</div>
|
||||
<div className="mt-1 space-y-1">
|
||||
{settings?.dashboardState?.welcome?.contentFolders?.map((folder: string) => (
|
||||
<Folder
|
||||
key={folder}
|
||||
folder={folder}
|
||||
addFolder={addFolder}
|
||||
wsFolder={settings.wsFolder}
|
||||
folders={settings.contentFolders}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className={`mt-4`}>
|
||||
<b>{l10n.t(LocalizationKey.commonInformation)}</b>: {l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedContentFoldersInformationDescription)}.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react';
|
||||
import { join } from 'path';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../../localization';
|
||||
import { ContentFolder } from '../../../../models';
|
||||
import { SelectItem } from '../../Steps/SelectItem';
|
||||
|
||||
export interface IFolderProps {
|
||||
wsFolder: string;
|
||||
folder: string;
|
||||
folders: ContentFolder[];
|
||||
addFolder: (folder: string) => void;
|
||||
}
|
||||
|
||||
export const Folder: React.FunctionComponent<IFolderProps> = ({
|
||||
wsFolder,
|
||||
folder,
|
||||
folders,
|
||||
addFolder
|
||||
}: React.PropsWithChildren<IFolderProps>) => {
|
||||
|
||||
const isAdded = React.useMemo(
|
||||
() => folders.find((f) => f.path.toLowerCase() === join(wsFolder, folder).toLowerCase()),
|
||||
[folder, folders, wsFolder]
|
||||
);
|
||||
|
||||
return (
|
||||
<SelectItem
|
||||
title={folder}
|
||||
buttonTitle={l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedButtonAddFolderTitle)}
|
||||
isSelected={!!isAdded}
|
||||
onClick={() => addFolder(folder)} />
|
||||
);
|
||||
};
|
||||
@@ -31,6 +31,7 @@ import { Navigation } from './Navigation';
|
||||
import { ProjectSwitcher } from './ProjectSwitcher';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { SettingsLink } from '../SettingsView/SettingsLink';
|
||||
|
||||
export interface IHeaderProps {
|
||||
header?: React.ReactNode;
|
||||
@@ -156,7 +157,11 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
|
||||
}`}>
|
||||
<Tabs onNavigate={updateView} />
|
||||
|
||||
<ProjectSwitcher />
|
||||
<div className='flex'>
|
||||
<ProjectSwitcher />
|
||||
|
||||
<SettingsLink onNavigate={updateView} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{location.pathname === routePaths.contents && (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import useThemeColors from '../../hooks/useThemeColors';
|
||||
import { NavigationType } from '../../models';
|
||||
|
||||
export interface ITabProps {
|
||||
@@ -14,7 +13,6 @@ export const Tab: React.FunctionComponent<ITabProps> = ({
|
||||
children
|
||||
}: React.PropsWithChildren<ITabProps>) => {
|
||||
const location = useLocation();
|
||||
const { getColors } = useThemeColors();
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { CogIcon } from '@heroicons/react/solid';
|
||||
import { NavigationType } from '../../models';
|
||||
|
||||
export interface ISettingsLinkProps {
|
||||
onNavigate: (navigationType: NavigationType) => void;
|
||||
}
|
||||
|
||||
export const SettingsLink: React.FunctionComponent<ISettingsLinkProps> = ({
|
||||
onNavigate
|
||||
}: React.PropsWithChildren<ISettingsLinkProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className="flex items-center mr-4 hover:text-[var(--vscode-textLink-activeForeground)]"
|
||||
title={`Settings`}
|
||||
onClick={() => onNavigate(NavigationType.Settings)}
|
||||
>
|
||||
<CogIcon className="h-4 w-4" />
|
||||
<span className='sr-only'>Settings</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as React from 'react';
|
||||
import { PageLayout } from '../Layout';
|
||||
import { SponsorMsg } from '../Layout/SponsorMsg';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { Spinner } from '../Common/Spinner';
|
||||
import { AstroContentTypes } from '../Configuration/Astro/AstroContentTypes';
|
||||
import { ContentFolders } from '../Configuration/Common/ContentFolders';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
|
||||
export interface ISettingsViewProps { }
|
||||
|
||||
export const SettingsView: React.FunctionComponent<ISettingsViewProps> = (_: React.PropsWithChildren<ISettingsViewProps>) => {
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{
|
||||
loading && <Spinner />
|
||||
}
|
||||
|
||||
{
|
||||
settings && (
|
||||
<div className='mx-auto max-w-7xl'>
|
||||
<h1 className='text-3xl'>{l10n.t(LocalizationKey.commonSettings)}</h1>
|
||||
|
||||
<div className='divide-y divide-[var(--frontmatter-border)]'>
|
||||
{
|
||||
settings?.crntFramework === 'astro' && (
|
||||
<div className='py-4'>
|
||||
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsContentTypes)}</h2>
|
||||
|
||||
<AstroContentTypes
|
||||
settings={settings}
|
||||
triggerLoading={(isLoading) => setLoading(isLoading)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className='py-4'>
|
||||
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsContentFolders)}</h2>
|
||||
|
||||
<ContentFolders
|
||||
settings={settings}
|
||||
triggerLoading={(isLoading) => setLoading(isLoading)} />
|
||||
</div>
|
||||
|
||||
<div className='py-4 space-y-2'>
|
||||
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsDiagnostic)}</h2>
|
||||
|
||||
<p>{l10n.t(LocalizationKey.settingsDiagnosticDescription)}</p>
|
||||
|
||||
<p>
|
||||
<a
|
||||
href={`command:frontMatter.diagnostics`}
|
||||
title={l10n.t(LocalizationKey.settingsDiagnosticLink)}
|
||||
className='text-[var(--vscode-textLink-foreground)] hover:text-[var(--vscode-textLink-activeForeground)]'
|
||||
>
|
||||
{l10n.t(LocalizationKey.settingsDiagnosticLink)}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<SponsorMsg
|
||||
beta={settings?.beta}
|
||||
version={settings?.versionInfo}
|
||||
isBacker={settings?.isBacker}
|
||||
/>
|
||||
|
||||
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=settings" alt="Settings metrics" />
|
||||
</PageLayout >
|
||||
);
|
||||
};
|
||||
@@ -7,6 +7,7 @@ export interface ISelectItemProps {
|
||||
icon?: "add" | "select";
|
||||
buttonTitle: string;
|
||||
isSelected: boolean;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
@@ -15,6 +16,7 @@ export const SelectItem: React.FunctionComponent<ISelectItemProps> = ({
|
||||
icon = "select",
|
||||
buttonTitle,
|
||||
isSelected,
|
||||
disabled,
|
||||
onClick
|
||||
}: React.PropsWithChildren<ISelectItemProps>) => {
|
||||
return (
|
||||
@@ -23,8 +25,9 @@ export const SelectItem: React.FunctionComponent<ISelectItemProps> = ({
|
||||
>
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`mr-2 flex gap-2 items-center hover:text-[var(--vscode-textLink-activeForeground)]`}
|
||||
className={`mr-2 flex gap-2 items-center hover:text-[var(--vscode-textLink-activeForeground)] disabled:cursor-not-allowed`}
|
||||
title={buttonTitle}
|
||||
disabled={disabled}
|
||||
>
|
||||
{isSelected ? (
|
||||
icon === "add" ? (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CheckIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import useThemeColors from '../../hooks/useThemeColors';
|
||||
import { Status } from '../../models/Status';
|
||||
|
||||
export interface IStepProps {
|
||||
@@ -18,7 +17,6 @@ export const Step: React.FunctionComponent<IStepProps> = ({
|
||||
showLine,
|
||||
onClick
|
||||
}: React.PropsWithChildren<IStepProps>) => {
|
||||
const { getColors } = useThemeColors();
|
||||
|
||||
const renderChildren = () => {
|
||||
return (
|
||||
@@ -30,21 +28,17 @@ export const Step: React.FunctionComponent<IStepProps> = ({
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`h-2.5 w-2.5 bg-transparent rounded-full ${onClick ? getColors('group-hover:bg-gray-400', 'group-hover:bg-[var(--vscode-button-hoverBackground)]') : ''
|
||||
className={`h-2.5 w-2.5 bg-transparent rounded-full ${onClick ? 'group-hover:bg-[var(--vscode-button-hoverBackground)]' : ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === Status.Active && (
|
||||
{(status === Status.Active || status === Status.Optional) && (
|
||||
<div className="h-9 flex items-center" aria-hidden="true">
|
||||
<div className={`relative z-10 w-8 h-8 flex items-center justify-center border-2 rounded-full bg-[var(--frontmatter-border)] border-[var(--frontmatter-border)] group-hover:text-[var(--vscode-button-foreground)] group-hover:border-[var(--vscode-button-hoverBackground)]`}>
|
||||
<span className={`h-2.5 w-2.5 rounded-full ${getColors(
|
||||
'bg-teal-600',
|
||||
'bg-[var(--frontmatter-button-background)]'
|
||||
)
|
||||
}`} />
|
||||
<span className={`h-2.5 w-2.5 rounded-full bg-[var(--frontmatter-button-background)]`} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -71,8 +65,7 @@ export const Step: React.FunctionComponent<IStepProps> = ({
|
||||
<>
|
||||
{showLine ? (
|
||||
<div
|
||||
className={`-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full ${status === Status.Completed ? getColors('bg-teal-600', 'bg-[var(--frontmatter-button-background)]') : getColors('bg-gray-300', 'bg-[var(--frontmatter-border)]')
|
||||
}`}
|
||||
className={`-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full ${(status === Status.Completed || status === Status.Optional) ? 'bg-[var(--frontmatter-button-background)]' : 'bg-[var(--frontmatter-border)]'}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -7,10 +7,9 @@ import { Step } from './Step';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { MenuItem } from '../Menu';
|
||||
import { ContentFolder, Framework, StaticFolder, Template } from '../../../models';
|
||||
import { Framework, StaticFolder, Template } from '../../../models';
|
||||
import { ChevronDownIcon } from '@heroicons/react/outline';
|
||||
import { FrameworkDetectors } from '../../../constants/FrameworkDetectors';
|
||||
import { join } from 'path';
|
||||
import useThemeColors from '../../hooks/useThemeColors';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
@@ -18,37 +17,13 @@ import { SelectItem } from './SelectItem';
|
||||
import { Templates } from '../../../constants';
|
||||
import { TemplateItem } from './TemplateItem';
|
||||
import { Spinner } from '../Common/Spinner';
|
||||
import { AstroContentTypes } from '../Configuration/Astro/AstroContentTypes';
|
||||
import { ContentFolders } from '../Configuration/Common/ContentFolders';
|
||||
|
||||
export interface IStepsToGetStartedProps {
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
const Folder = ({
|
||||
wsFolder,
|
||||
folder,
|
||||
folders,
|
||||
addFolder
|
||||
}: {
|
||||
wsFolder: string;
|
||||
folder: string;
|
||||
folders: ContentFolder[];
|
||||
addFolder: (folder: string) => void;
|
||||
}) => {
|
||||
|
||||
const isAdded = useMemo(
|
||||
() => folders.find((f) => f.path.toLowerCase() === join(wsFolder, folder).toLowerCase()),
|
||||
[folder, folders, wsFolder]
|
||||
);
|
||||
|
||||
return (
|
||||
<SelectItem
|
||||
title={folder}
|
||||
buttonTitle={l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedButtonAddFolderTitle)}
|
||||
isSelected={!!isAdded}
|
||||
onClick={() => addFolder(folder)} />
|
||||
);
|
||||
};
|
||||
|
||||
export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps> = ({
|
||||
settings
|
||||
}: React.PropsWithChildren<IStepsToGetStartedProps>) => {
|
||||
@@ -65,10 +40,6 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
|
||||
Messenger.send(DashboardMessage.setFramework, framework);
|
||||
};
|
||||
|
||||
const addFolder = (folder: string) => {
|
||||
Messenger.send(DashboardMessage.addFolder, folder);
|
||||
};
|
||||
|
||||
const addAssetFolder = (folder: string | StaticFolder) => {
|
||||
Messenger.send(DashboardMessage.addAssetsFolder, folder);
|
||||
}
|
||||
@@ -207,13 +178,38 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
|
||||
</div>
|
||||
),
|
||||
show: (crntTemplates || []).length > 0,
|
||||
status: Status.Active,
|
||||
status: Status.Optional,
|
||||
},
|
||||
{
|
||||
id: `astro-content-types`,
|
||||
name: l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedAstroContentTypesName),
|
||||
description: (
|
||||
<AstroContentTypes
|
||||
settings={settings}
|
||||
triggerLoading={(isLoading) => setLoading(isLoading)} />
|
||||
),
|
||||
show: settings.crntFramework === 'astro' || framework === 'astro',
|
||||
status: Status.Optional
|
||||
},
|
||||
{
|
||||
id: `welcome-content-folders`,
|
||||
name: l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedContentFoldersName),
|
||||
description: (
|
||||
<ContentFolders
|
||||
settings={settings}
|
||||
triggerLoading={(isLoading) => setLoading(isLoading)} />
|
||||
),
|
||||
show: true,
|
||||
status:
|
||||
settings.contentFolders && settings.contentFolders.length > 0
|
||||
? Status.Completed
|
||||
: Status.NotStarted
|
||||
},
|
||||
{
|
||||
id: `welcome-assets`,
|
||||
name: l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedAssetsFolderName),
|
||||
description: (
|
||||
<div className='mt-4'>
|
||||
<div className='mt-1'>
|
||||
<div className="text-sm">{l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedAssetsFolderDescription)}</div>
|
||||
<div className="mt-1 space-y-1">
|
||||
<SelectItem
|
||||
@@ -239,43 +235,6 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
|
||||
show: settings.crntFramework === 'astro' || framework === 'astro',
|
||||
status: settings.initialized && settings.staticFolder && settings.staticFolder !== "/" ? Status.Completed : Status.NotStarted,
|
||||
},
|
||||
{
|
||||
id: `welcome-content-folders`,
|
||||
name: l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedContentFoldersName),
|
||||
description: (
|
||||
<>
|
||||
<p>
|
||||
{l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedContentFoldersDescription)}
|
||||
</p>
|
||||
|
||||
{settings?.dashboardState?.welcome?.contentFolders?.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<div className="text-sm">{l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedContentFoldersLabel)}</div>
|
||||
<div className="mt-1 space-y-1">
|
||||
{settings?.dashboardState?.welcome?.contentFolders?.map((folder: string) => (
|
||||
<Folder
|
||||
key={folder}
|
||||
folder={folder}
|
||||
addFolder={addFolder}
|
||||
wsFolder={settings.wsFolder}
|
||||
folders={settings.contentFolders}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className={`mt-4 ${getColors('text-vulcan-300 dark:text-gray-400', '')}`}>
|
||||
<b>{l10n.t(LocalizationKey.commonInformation)}</b>: {l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedContentFoldersInformationDescription)}.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
show: true,
|
||||
status:
|
||||
settings.contentFolders && settings.contentFolders.length > 0
|
||||
? Status.Completed
|
||||
: Status.NotStarted
|
||||
},
|
||||
{
|
||||
id: `welcome-import`,
|
||||
name: l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedTagsName),
|
||||
|
||||
@@ -45,7 +45,8 @@ export const routePaths: { [name: string]: string } = {
|
||||
media: '/media',
|
||||
snippets: '/snippets',
|
||||
data: '/data',
|
||||
taxonomy: '/taxonomy'
|
||||
taxonomy: '/taxonomy',
|
||||
settings: '/settings',
|
||||
};
|
||||
|
||||
const preserveColor = (color: string | undefined) => {
|
||||
|
||||
@@ -4,5 +4,6 @@ export enum NavigationType {
|
||||
Media = 'media',
|
||||
Data = 'data',
|
||||
Snippets = 'snippets',
|
||||
Taxonomy = 'taxonomy'
|
||||
Taxonomy = 'taxonomy',
|
||||
Settings = 'settings'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export enum Status {
|
||||
Active = 1,
|
||||
Completed,
|
||||
NotStarted
|
||||
NotStarted,
|
||||
Optional
|
||||
}
|
||||
|
||||
@@ -626,36 +626,41 @@ export class Settings {
|
||||
}
|
||||
|
||||
// Check if there is a dynamic config file and use it to update the global config
|
||||
const dynamicConfigPath =
|
||||
Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CONFIG_DYNAMIC_FILE_PATH}`];
|
||||
if (dynamicConfigPath) {
|
||||
try {
|
||||
await window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: `${EXTENSION_NAME}: Reading dynamic config file...`
|
||||
},
|
||||
async () => {
|
||||
const absFilePath = Folders.getAbsFilePath(dynamicConfigPath);
|
||||
Logger.info(`Reading dynamic config file: ${absFilePath}`);
|
||||
if (absFilePath) {
|
||||
if (await existsAsync(absFilePath)) {
|
||||
const configFunction = require(absFilePath);
|
||||
const dynamicConfig = await configFunction(
|
||||
Object.assign({}, Settings.globalConfig)
|
||||
);
|
||||
if (
|
||||
Settings.globalConfig &&
|
||||
Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CONFIG_DYNAMIC_FILE_PATH}`]
|
||||
) {
|
||||
const dynamicConfigPath =
|
||||
Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CONFIG_DYNAMIC_FILE_PATH}`];
|
||||
if (dynamicConfigPath) {
|
||||
try {
|
||||
await window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: `${EXTENSION_NAME}: Reading dynamic config file...`
|
||||
},
|
||||
async () => {
|
||||
const absFilePath = Folders.getAbsFilePath(dynamicConfigPath);
|
||||
Logger.info(`Reading dynamic config file: ${absFilePath}`);
|
||||
if (absFilePath) {
|
||||
if (await existsAsync(absFilePath)) {
|
||||
const configFunction = require(absFilePath);
|
||||
const dynamicConfig = await configFunction(
|
||||
Object.assign({}, Settings.globalConfig)
|
||||
);
|
||||
|
||||
if (dynamicConfig) {
|
||||
Settings.globalConfig = dynamicConfig;
|
||||
Logger.info(`Dynamic config file loaded`);
|
||||
if (dynamicConfig) {
|
||||
Settings.globalConfig = dynamicConfig;
|
||||
Logger.info(`Dynamic config file loaded`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
Logger.error(`Error reading dynamic config file: ${dynamicConfigPath}`);
|
||||
Logger.error((e as Error).message);
|
||||
);
|
||||
} catch (e) {
|
||||
Logger.error(`Error reading dynamic config file: ${dynamicConfigPath}`);
|
||||
Logger.error((e as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
import { Uri, workspace } from 'vscode';
|
||||
import { DashboardMessage } from '../../dashboardWebView/DashboardMessage';
|
||||
import { AstroCollection, AstroField, ContentType, Field, PostMessageData } from '../../models';
|
||||
import { BaseListener } from './BaseListener';
|
||||
import { exec } from 'child_process';
|
||||
import { Extension, Logger, Settings } from '../../helpers';
|
||||
import { Folders } from '../../commands';
|
||||
import { SETTING_TAXONOMY_CONTENT_TYPES, SsgScripts } from '../../constants';
|
||||
import { SettingsListener } from './SettingsListener';
|
||||
import { Terminal } from '../../services';
|
||||
|
||||
export class SsgListener extends BaseListener {
|
||||
/**
|
||||
* Process the messages for the dashboard views
|
||||
* @param msg
|
||||
*/
|
||||
public static process(msg: PostMessageData) {
|
||||
super.process(msg);
|
||||
|
||||
switch (msg.command) {
|
||||
case DashboardMessage.ssgGetAstroContentTypes:
|
||||
SsgListener.getAstroContentTypes(msg);
|
||||
break;
|
||||
case DashboardMessage.ssgSetAstroContentTypes:
|
||||
SsgListener.ssgSetAstroContentTypes(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creata a content type from the Astro content collection
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
private static async ssgSetAstroContentTypes({ command, requestId, payload }: PostMessageData) {
|
||||
if (!command || !requestId || !payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { collection } = payload as { collection: AstroCollection };
|
||||
|
||||
const contentType: ContentType = {
|
||||
name: collection.name,
|
||||
filePrefix: undefined,
|
||||
previewPath: undefined,
|
||||
pageBundle: false,
|
||||
clearEmpty: true,
|
||||
fields: []
|
||||
};
|
||||
|
||||
for (const field of collection.fields) {
|
||||
const ctField = SsgListener.getCtFieldForAstroField(field, collection.fields);
|
||||
|
||||
if (ctField) {
|
||||
contentType.fields.push(ctField);
|
||||
}
|
||||
}
|
||||
|
||||
const contentTypes = Settings.get<ContentType[]>(SETTING_TAXONOMY_CONTENT_TYPES) || [];
|
||||
|
||||
if (contentTypes.find((ct) => ct.name === collection.name)) {
|
||||
SsgListener.sendRequest(command as any, requestId, {});
|
||||
SettingsListener.getSettings(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if the content folder exists
|
||||
const folderName = `/src/content/${name}`;
|
||||
const folderUri = Uri.joinPath(Folders.getWorkspaceFolder()!, folderName);
|
||||
await workspace.fs.readDirectory(folderUri);
|
||||
|
||||
await Folders.register({
|
||||
title: collection.name,
|
||||
path: folderUri,
|
||||
contentType: [collection.name]
|
||||
});
|
||||
|
||||
contentType.previewPath = `'${name}'`;
|
||||
} catch (e) {
|
||||
// Something failed, so it seems the folder does not exist
|
||||
}
|
||||
|
||||
contentTypes.push(contentType);
|
||||
await Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
|
||||
|
||||
SsgListener.sendRequest(command as any, requestId, {});
|
||||
SettingsListener.getSettings(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Astro content types from the local project
|
||||
* @param command
|
||||
* @param requestId
|
||||
*/
|
||||
private static async getAstroContentTypes({ command, requestId }: PostMessageData) {
|
||||
if (!command || !requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// https://github.com/withastro/astro/blob/defab70cb2a0c67d5e9153542490d2749046b151/packages/astro/src/content/utils.ts#L450
|
||||
const contentConfig = await workspace.findFiles(`**/src/content/config.*`);
|
||||
|
||||
if (contentConfig.length === 0) {
|
||||
SsgListener.sendRequest(command as any, requestId, {});
|
||||
return;
|
||||
}
|
||||
|
||||
const contentConfigFile = contentConfig[0];
|
||||
if (
|
||||
!contentConfigFile.fsPath.endsWith('.js') &&
|
||||
!contentConfigFile.fsPath.endsWith('.mjs') &&
|
||||
!contentConfigFile.fsPath.endsWith('.ts') &&
|
||||
!contentConfigFile.fsPath.endsWith('.mts')
|
||||
) {
|
||||
SsgListener.sendRequest(command as any, requestId, {});
|
||||
return;
|
||||
}
|
||||
|
||||
const scriptPath = Uri.joinPath(
|
||||
Extension.getInstance().extensionPath,
|
||||
SsgScripts.folder,
|
||||
SsgScripts.astroContentCollection
|
||||
);
|
||||
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
if (!wsFolder) {
|
||||
SsgListener.sendRequest(command as any, requestId, {});
|
||||
return;
|
||||
}
|
||||
|
||||
const tempLocation = Uri.joinPath(wsFolder, '/.frontmatter/temp');
|
||||
const tempScriptPath = Uri.joinPath(tempLocation, SsgScripts.astroContentCollection);
|
||||
await workspace.fs.createDirectory(tempLocation);
|
||||
workspace.fs.copy(scriptPath, tempScriptPath, { overwrite: true });
|
||||
|
||||
const fullScript = `node "${tempScriptPath.fsPath}" "${contentConfigFile.fsPath}"`;
|
||||
|
||||
try {
|
||||
const result: string = await SsgListener.executeScript(fullScript, wsFolder?.fsPath || '');
|
||||
if (result) {
|
||||
let collections: AstroCollection[] = JSON.parse(result);
|
||||
collections = collections.filter((c) => c.type === 'content');
|
||||
SsgListener.sendRequest(command as any, requestId, collections);
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error((error as Error).message);
|
||||
SsgListener.sendRequest(command as any, requestId, {});
|
||||
return;
|
||||
} finally {
|
||||
await workspace.fs.delete(tempLocation, { recursive: true, useTrash: false });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the ContentType field from the Astro field
|
||||
* @param field
|
||||
* @param collection
|
||||
* @returns
|
||||
*/
|
||||
private static getCtFieldForAstroField(
|
||||
field: AstroField,
|
||||
collection: AstroField[]
|
||||
): Field | undefined {
|
||||
let ctField: Field | undefined = undefined;
|
||||
const hasImageField = collection.find((f) => f.type === 'image');
|
||||
|
||||
switch (field.type) {
|
||||
case 'email':
|
||||
case 'url':
|
||||
case 'ZodString':
|
||||
ctField = {
|
||||
name: field.name,
|
||||
type: 'string',
|
||||
single: true
|
||||
} as Field;
|
||||
break;
|
||||
case 'ZodNumber':
|
||||
ctField = {
|
||||
name: field.name,
|
||||
type: 'number'
|
||||
} as Field;
|
||||
break;
|
||||
case 'ZodBoolean':
|
||||
ctField = {
|
||||
name: field.name,
|
||||
type: 'boolean'
|
||||
} as Field;
|
||||
break;
|
||||
case 'ZodArray':
|
||||
ctField = {
|
||||
name: field.name,
|
||||
type: 'list'
|
||||
} as Field;
|
||||
break;
|
||||
case 'ZodEnum':
|
||||
ctField = {
|
||||
name: field.name,
|
||||
type: 'choice',
|
||||
choices: field.options || []
|
||||
} as Field;
|
||||
break;
|
||||
case 'ZodDate':
|
||||
ctField = {
|
||||
name: field.name,
|
||||
type: 'datetime',
|
||||
default: '{{now}}'
|
||||
} as Field;
|
||||
break;
|
||||
case 'image':
|
||||
ctField = {
|
||||
name: field.name,
|
||||
type: 'image'
|
||||
} as Field;
|
||||
break;
|
||||
case 'ZodObject':
|
||||
ctField = {
|
||||
name: field.name,
|
||||
type: 'fields'
|
||||
} as Field;
|
||||
break;
|
||||
}
|
||||
|
||||
if (field.type === 'ZodObject' && field.fields) {
|
||||
ctField.fields = [];
|
||||
for (const subField of field.fields) {
|
||||
const ctSubField = SsgListener.getCtFieldForAstroField(subField, collection);
|
||||
|
||||
if (ctSubField) {
|
||||
ctField.fields.push(ctSubField);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ctField && field.required) {
|
||||
ctField.required = field.required;
|
||||
}
|
||||
|
||||
if (ctField && field.defaultValue) {
|
||||
ctField.default = field.defaultValue;
|
||||
}
|
||||
|
||||
if (ctField && field.description) {
|
||||
ctField.description = field.description;
|
||||
}
|
||||
|
||||
if (ctField && field.type === 'image' && !hasImageField) {
|
||||
ctField.isPreviewImage = true;
|
||||
}
|
||||
|
||||
return ctField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a script
|
||||
* @param fullScript
|
||||
* @param wsFolder
|
||||
* @returns
|
||||
*/
|
||||
private static executeScript(fullScript: string, wsFolder: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
Logger.info(`Executing script: ${fullScript}`);
|
||||
|
||||
const shellPath: string | { path: string } | undefined = Terminal.shell;
|
||||
|
||||
exec(
|
||||
fullScript,
|
||||
{
|
||||
cwd: wsFolder,
|
||||
shell: shellPath
|
||||
},
|
||||
(error, stdout) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stdout && stdout.endsWith(`\n`)) {
|
||||
// Remove empty line at the end of the string
|
||||
stdout = stdout.slice(0, -1);
|
||||
}
|
||||
|
||||
resolve(stdout);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,3 +10,4 @@ export * from './TelemetryListener';
|
||||
export * from './TaxonomyListener';
|
||||
export * from './LogListener';
|
||||
export * from './LocalizationListener';
|
||||
export * from './SsgListener';
|
||||
|
||||
@@ -95,6 +95,30 @@ export enum LocalizationKey {
|
||||
* Open on website
|
||||
*/
|
||||
commonOpenOnWebsite = 'common.openOnWebsite',
|
||||
/**
|
||||
* Settings
|
||||
*/
|
||||
commonSettings = 'common.settings',
|
||||
/**
|
||||
* Content types
|
||||
*/
|
||||
settingsContentTypes = 'settings.contentTypes',
|
||||
/**
|
||||
* Content folders
|
||||
*/
|
||||
settingsContentFolders = 'settings.contentFolders',
|
||||
/**
|
||||
* Diagnostic
|
||||
*/
|
||||
settingsDiagnostic = 'settings.diagnostic',
|
||||
/**
|
||||
* You can run the diagnostics to check the whole Front Matter CMS configuration.
|
||||
*/
|
||||
settingsDiagnosticDescription = 'settings.diagnostic.description',
|
||||
/**
|
||||
* Run full diagnostics
|
||||
*/
|
||||
settingsDiagnosticLink = 'settings.diagnostic.link',
|
||||
/**
|
||||
* Developer mode
|
||||
*/
|
||||
@@ -807,6 +831,10 @@ export enum LocalizationKey {
|
||||
* Select a template to prefill the frontmatter.json file with the recommended settings.
|
||||
*/
|
||||
dashboardStepsStepsToGetStartedTemplateDescription = 'dashboard.steps.stepsToGetStarted.template.description',
|
||||
/**
|
||||
* Create Content-Types for your Astro Content Collections
|
||||
*/
|
||||
dashboardStepsStepsToGetStartedAstroContentTypesName = 'dashboard.steps.stepsToGetStarted.astroContentTypes.name',
|
||||
/**
|
||||
* Add {0} to taxonomy settings
|
||||
*/
|
||||
@@ -935,6 +963,14 @@ export enum LocalizationKey {
|
||||
* Do you want to remap the metadata of unmapped files?
|
||||
*/
|
||||
dashboardMediaDetailsSlideOverUnmappedDescription = 'dashboard.media.detailsSlideOver.unmapped.description',
|
||||
/**
|
||||
* No Astro Content Collections found.
|
||||
*/
|
||||
dashboardConfigurationAstroAstroContentTypesEmpty = 'dashboard.configuration.astro.astroContentTypes.empty',
|
||||
/**
|
||||
* The following Astro Content Collections and can be used to generate a content-type.
|
||||
*/
|
||||
dashboardConfigurationAstroAstroContentTypesDescription = 'dashboard.configuration.astro.astroContentTypes.description',
|
||||
/**
|
||||
* Content-type
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export interface AstroCollection {
|
||||
name: string;
|
||||
type: 'content' | 'data';
|
||||
fields: AstroField[];
|
||||
}
|
||||
|
||||
export interface AstroField {
|
||||
name: string;
|
||||
type:
|
||||
| 'ZodString'
|
||||
| 'ZodNumber'
|
||||
| 'ZodBoolean'
|
||||
| 'ZodArray'
|
||||
| 'ZodEnum'
|
||||
| 'ZodDate'
|
||||
| 'ZodObject'
|
||||
| 'email'
|
||||
| 'url'
|
||||
| 'image';
|
||||
required: boolean;
|
||||
defaultValue?: string;
|
||||
options?: string[];
|
||||
description?: string;
|
||||
fields?: AstroField[];
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface PostMessageData {
|
||||
command: string;
|
||||
payload: any;
|
||||
requestId?: string
|
||||
}
|
||||
export interface PostMessageData {
|
||||
command: string;
|
||||
payload: any;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './AstroCollections';
|
||||
export * from './BaseFieldProps';
|
||||
export * from './BlockFieldData';
|
||||
export * from './Choice';
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { workspace } from 'vscode';
|
||||
import * as os from 'os';
|
||||
|
||||
interface ShellSetting {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export class Terminal {
|
||||
/**
|
||||
* Return the shell path for the current platform
|
||||
*/
|
||||
public static get shell() {
|
||||
const shell: string | { path: string } | undefined = Terminal.getShellPath();
|
||||
let shellPath: string | undefined = undefined;
|
||||
|
||||
if (typeof shell !== 'string' && !!shell) {
|
||||
shellPath = shell.path;
|
||||
} else {
|
||||
shellPath = shell || undefined;
|
||||
}
|
||||
|
||||
return shellPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the automation profile for the current platform
|
||||
* @returns
|
||||
*/
|
||||
private static getShellPath(): string | ShellSetting | undefined {
|
||||
const platform = Terminal.getPlatform();
|
||||
const terminalSettings = workspace.getConfiguration('terminal');
|
||||
|
||||
const automationProfile = terminalSettings.get<string | ShellSetting>(
|
||||
`integrated.automationProfile.${platform}`
|
||||
);
|
||||
if (!!automationProfile) {
|
||||
return automationProfile;
|
||||
}
|
||||
|
||||
const defaultProfile = terminalSettings.get<string>(`integrated.defaultProfile.${platform}`);
|
||||
const profiles = terminalSettings.get<{ [prop: string]: ShellSetting }>(
|
||||
`integrated.profiles.${platform}`
|
||||
);
|
||||
|
||||
if (defaultProfile && profiles && profiles[defaultProfile]) {
|
||||
return profiles[defaultProfile];
|
||||
}
|
||||
|
||||
return terminalSettings.get(`integrated.shell.${platform}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current platform
|
||||
* @returns
|
||||
*/
|
||||
private static getPlatform = (): 'windows' | 'linux' | 'osx' => {
|
||||
const platform = os.platform();
|
||||
if (platform === 'win32') {
|
||||
return 'windows';
|
||||
} else if (platform === 'darwin') {
|
||||
return 'osx';
|
||||
}
|
||||
|
||||
return 'linux';
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './Credentials';
|
||||
export * from './ModeSwitch';
|
||||
export * from './PagesParser';
|
||||
export * from './SponsorAI';
|
||||
export * from './Terminal';
|
||||
@@ -0,0 +1,217 @@
|
||||
import { createServer } from "vite";
|
||||
import zod from "zod";
|
||||
|
||||
const {
|
||||
ZodDefault,
|
||||
ZodObject,
|
||||
ZodOptional,
|
||||
ZodString,
|
||||
ZodEffects,
|
||||
ZodEnum
|
||||
} = zod;
|
||||
|
||||
/**
|
||||
* Process the Zod field
|
||||
* @param {ZodTypeAny} field
|
||||
* @param {string} defaultValue
|
||||
* @returns
|
||||
*/
|
||||
function getField(field, defaultValue = undefined) {
|
||||
let isOptional = false;
|
||||
|
||||
// Handle various type transformations and assignments
|
||||
if (field instanceof ZodOptional) {
|
||||
isOptional = true;
|
||||
const type = field.unwrap();
|
||||
|
||||
return getField(type, isOptional, defaultValue)
|
||||
}
|
||||
|
||||
if (field instanceof ZodDefault) {
|
||||
// https://github.com/colinhacks/zod/blob/master/README.md#default
|
||||
isOptional = true;
|
||||
defaultValue = field.parse(undefined);
|
||||
// https://github.com/sachinraja/zod-to-ts/blob/main/src/index.ts
|
||||
const type = field._def.innerType;
|
||||
return getField(type, isOptional, defaultValue)
|
||||
}
|
||||
|
||||
return {
|
||||
type: field,
|
||||
isOptional,
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the field information
|
||||
* @param {string} name
|
||||
* @param {ZodTypeAny} type
|
||||
* @returns
|
||||
*/
|
||||
function generateFieldInfo(name, type) {
|
||||
let description = type.description;
|
||||
let defaultValue = undefined;
|
||||
|
||||
const {
|
||||
type: fieldType,
|
||||
isOptional: isFieldOptional,
|
||||
defaultValue: fieldDefaultValue
|
||||
} = getField(type, defaultValue);
|
||||
|
||||
const fieldInfo = {
|
||||
name: name,
|
||||
description: description,
|
||||
defaultValue: fieldDefaultValue,
|
||||
type: fieldType._def.typeName,
|
||||
required: !isFieldOptional,
|
||||
};
|
||||
|
||||
if (fieldType instanceof ZodObject) {
|
||||
const subFields = extractFieldInfoFromShape(fieldType);
|
||||
|
||||
fieldInfo.fields = subFields;
|
||||
}
|
||||
|
||||
if (fieldType instanceof ZodEffects) {
|
||||
fieldInfo.type = fieldType.sourceType().fmFieldType;
|
||||
}
|
||||
|
||||
if (fieldType instanceof ZodEnum) {
|
||||
fieldInfo.options = fieldType.options;
|
||||
}
|
||||
|
||||
if (fieldType instanceof ZodString) {
|
||||
// https://github.com/StefanTerdell/zod-to-json-schema/blob/master/src/parsers/string.ts#L45
|
||||
if (fieldType._def.checks && fieldType._def.checks.length > 0) {
|
||||
const check = fieldType._def.checks.pop();
|
||||
fieldInfo.type = check.kind;
|
||||
}
|
||||
}
|
||||
|
||||
return fieldInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the scheme into an array of fields
|
||||
* @param {ZodTypeAny} type
|
||||
* @returns
|
||||
*/
|
||||
function extractFieldInfoFromShape(type) {
|
||||
if (type instanceof ZodOptional) {
|
||||
type = type.unwrap();
|
||||
}
|
||||
|
||||
if (!(type instanceof ZodObject)) {
|
||||
// Return an empty array if the type is not of the expected type
|
||||
return [];
|
||||
}
|
||||
|
||||
// Iterate through the shape properties
|
||||
// https://github.com/sachinraja/zod-to-ts/blob/1389b33557bcca8a02da66cd5c48efbe7579720c/src/index.ts#L134
|
||||
const properties = Object.entries(type._def.shape());
|
||||
const fieldInfoList = properties.map(([fieldName, fieldShape]) => {
|
||||
return generateFieldInfo(fieldName, fieldShape);
|
||||
});
|
||||
|
||||
return fieldInfoList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process each content collection
|
||||
* @param {*} collections
|
||||
* @returns
|
||||
*/
|
||||
function processCollection(collections) {
|
||||
if (!Array.isArray(collections)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collections.map(([name, collection]) => {
|
||||
const schema = typeof collection.schema === "function" ? collection.schema({
|
||||
image() {
|
||||
const field = zod.string();
|
||||
field.fmFieldType = "image";
|
||||
return field;
|
||||
}
|
||||
}) : collection.schema;
|
||||
|
||||
return {
|
||||
name,
|
||||
type: collection.type || "content",
|
||||
fields: extractFieldInfoFromShape(schema),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* More info: https://vitejs.dev/guide/api-plugin.html#virtual-modules-convention
|
||||
* @returns
|
||||
*/
|
||||
const astroContentModulePlugin = () => {
|
||||
const astroContent = "astro:content";
|
||||
const astroContentMarker = `\0${astroContent}`;
|
||||
|
||||
return {
|
||||
name: "astro-content-collections",
|
||||
resolveId(importee) {
|
||||
if (importee === astroContent) {
|
||||
return astroContentMarker;
|
||||
}
|
||||
},
|
||||
load(id) {
|
||||
if (id === astroContentMarker) {
|
||||
// https://github.com/withastro/astro/blob/defab70cb2a0c67d5e9153542490d2749046b151/packages/astro/content-module.template.mjs#L12
|
||||
return `
|
||||
export { z } from 'astro/zod';
|
||||
// Reference: /node_modules/astro/content-module.template.mjs
|
||||
export function defineCollection(config) {
|
||||
if (!config.type) config.type = 'content';
|
||||
return config;
|
||||
};
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Start of the Astro Collections script
|
||||
*/
|
||||
(async () => {
|
||||
const configPath = process.argv[2];
|
||||
if (!configPath || typeof configPath !== "string") {
|
||||
console.log("No config path provided");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// https://vitejs.dev/guide/ssr.html#setting-up-the-dev-server
|
||||
// https://github.com/withastro/astro/blob/defab70cb2a0c67d5e9153542490d2749046b151/packages/astro/src/core/config/vite-load.ts#L8
|
||||
const server = await createServer({
|
||||
server: {
|
||||
middlewareMode: true,
|
||||
hmr: false
|
||||
},
|
||||
optimizeDeps: {
|
||||
disabled: true,
|
||||
},
|
||||
appType: "custom",
|
||||
plugins: [astroContentModulePlugin()]
|
||||
});
|
||||
|
||||
try {
|
||||
// https://github.dev/withastro/astro/blob/defab70cb2a0c67d5e9153542490d2749046b151/packages/astro/src/content/utils.ts#L334
|
||||
const contentModule = await server.ssrLoadModule(configPath)
|
||||
|
||||
const collections = Object.entries(contentModule.collections ?? {});
|
||||
const processedCollections = processCollection(collections);
|
||||
|
||||
console.log(JSON.stringify(processedCollections));
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user