diff --git a/.changeset/afraid-radios-taste.md b/.changeset/afraid-radios-taste.md new file mode 100644 index 0000000000..cb69f91dce --- /dev/null +++ b/.changeset/afraid-radios-taste.md @@ -0,0 +1,6 @@ +--- +"@quri/ui": patch +--- + +- `WrenchIcon` +- `DropdownMenuLinkItem` for `` links diff --git a/.changeset/chilly-seals-float.md b/.changeset/chilly-seals-float.md new file mode 100644 index 0000000000..76c0fb9a24 --- /dev/null +++ b/.changeset/chilly-seals-float.md @@ -0,0 +1,5 @@ +--- +"@quri/ui": patch +--- + +Expose DropdownMenuItemLayout component diff --git a/.changeset/config.json b/.changeset/config.json index 319eae0e2f..eb7edc427b 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -12,6 +12,11 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["squiggle-website", "@quri/hub", "@quri/ops"], - "changelog": "../packages/ops/dist/changelog.js" + "ignore": [ + "squiggle-website", + "@quri/hub", + "@quri/ops", + "@quri/versioned-playground" + ], + "changelog": "../packages/ops/dist/scripts/changelog.cjs" } diff --git a/.changeset/fast-feet-happen.md b/.changeset/fast-feet-happen.md new file mode 100644 index 0000000000..bbe7323393 --- /dev/null +++ b/.changeset/fast-feet-happen.md @@ -0,0 +1,5 @@ +--- +"@quri/ui": patch +--- + +FormField and ControlledFormField components accept standaloneLabel prop diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b7316abe62..b8e62af277 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,12 @@ name: Release -on: workflow_dispatch +on: + workflow_dispatch: + inputs: + force-post-publish: + type: boolean + description: "Force post-publish script and PR even if nothing was published" + default: false # copy-pasted from ci.yml env: @@ -50,8 +56,25 @@ jobs: with: title: New release version: pnpm run changeset-version - publish: pnpm run publish + publish: pnpm run publish-all env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} VSCE_PAT: ${{ secrets.VSCE_PAT }} + + - name: Extract published version + id: published-version + run: | + VERSION=$(cat packages/squiggle-lang/package.json | fgrep '"version"' | head -1 | perl -ne '/"version": "([^"]+)"/ && print $1') + echo "published-version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Prepare for next release + if: steps.changesets.outputs.published == 'true' || inputs.force-post-publish + run: pnpm run post-publish + + - name: Create PR with version patches + if: steps.changesets.outputs.published == 'true' || inputs.force-post-publish + uses: peter-evans/create-pull-request@v5 + with: + commit-message: Bump versions after ${{ steps.published-version.outputs.published-version }} release + title: Bump versions after ${{ steps.published-version.outputs.published-version }} release diff --git a/.gitignore b/.gitignore index bc92f33c99..f18fa2b910 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ todo.txt result shell.nix .turbo +*.tsbuildinfo .pnp.* .yarn/* diff --git a/package.json b/package.json index 9eaf47d8c9..d3129aa6bb 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "scripts": { "nodeclean": "rm -r node_modules && rm -r packages/*/node_modules", "preinstall": "npx only-allow pnpm", - "changeset-version": "cd packages/ops && pnpm run changeset-version", - "changeset": "changeset", - "publish": "turbo lint build && changeset publish && cd packages/vscode-ext && pnpm run package && npx vsce publish --no-dependencies --skip-duplicate" + "changeset-version": "cd packages/ops && turbo build && pnpm run changeset-version", + "publish-all": "cd packages/ops && turbo build && pnpm run publish-all", + "post-publish": "cd packages/ops && turbo build && pnpm run post-publish" }, "devDependencies": { "@changesets/cli": "^2.26.2", diff --git a/packages/components/.gitignore b/packages/components/.gitignore index 88719277be..4e81c6d247 100644 --- a/packages/components/.gitignore +++ b/packages/components/.gitignore @@ -25,4 +25,3 @@ storybook-static dist /.vscode -/*.tsbuildinfo diff --git a/packages/hub/package.json b/packages/hub/package.json index 0bc51427b3..99f8033d3e 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -30,6 +30,7 @@ "@quri/squiggle-components": "workspace:*", "@quri/squiggle-lang": "workspace:*", "@quri/ui": "workspace:*", + "@quri/versioned-playground": "workspace:*", "@tailwindcss/typography": "^0.5.9", "base64-js": "^1.5.1", "clsx": "^2.0.0", diff --git a/packages/hub/prisma/migrations/20230823211259_squiggle_version/migration.sql b/packages/hub/prisma/migrations/20230823211259_squiggle_version/migration.sql new file mode 100644 index 0000000000..ddf6141475 --- /dev/null +++ b/packages/hub/prisma/migrations/20230823211259_squiggle_version/migration.sql @@ -0,0 +1,3 @@ +ALTER TABLE "SquiggleSnippet" ADD COLUMN "version" TEXT; +UPDATE "SquiggleSnippet" SET "version" = '0.8.5' WHERE "version" IS NULL; +ALTER TABLE "SquiggleSnippet" ALTER COLUMN "version" SET NOT NULL; diff --git a/packages/hub/prisma/schema.prisma b/packages/hub/prisma/schema.prisma index bab29af7f7..42f0d0df51 100644 --- a/packages/hub/prisma/schema.prisma +++ b/packages/hub/prisma/schema.prisma @@ -215,7 +215,8 @@ model ModelRevision { model SquiggleSnippet { id String @id @default(cuid()) - code String + code String + version String revision ModelRevision? } diff --git a/packages/hub/schema.graphql b/packages/hub/schema.graphql index c5822b19d0..377868c460 100644 --- a/packages/hub/schema.graphql +++ b/packages/hub/schema.graphql @@ -243,6 +243,7 @@ input MutationCreateSquiggleSnippetModelInput { """Defaults to false""" isPrivate: Boolean slug: String! + version: String! } union MutationCreateSquiggleSnippetModelResult = BaseError | CreateSquiggleSnippetModelResult | ValidationError @@ -336,8 +337,7 @@ input MutationUpdateRelativeValuesDefinitionInput { union MutationUpdateRelativeValuesDefinitionResult = BaseError | UpdateRelativeValuesDefinitionResult input MutationUpdateSquiggleSnippetModelInput { - code: String @deprecated(reason: "Use content arg instead") - content: SquiggleSnippetContentInput + content: SquiggleSnippetContentInput! owner: String! relativeValuesExports: [RelativeValuesExportInput!] slug: String! @@ -523,10 +523,12 @@ interface SquiggleOutput { type SquiggleSnippet implements Node { code: String! id: ID! + version: String! } input SquiggleSnippetContentInput { code: String! + version: String! } type UpdateGroupInviteRoleResult { diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx index cd4a955359..8274a68bc9 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx @@ -1,12 +1,9 @@ -import { FC, useMemo } from "react"; +import { FC, useMemo, useState } from "react"; import { FormProvider, useFieldArray, useForm } from "react-hook-form"; import { graphql, useFragment } from "react-relay"; -import { - PlaygroundToolbarItem, - SquigglePlayground, -} from "@quri/squiggle-components"; -import { Button, LinkIcon, useToast } from "@quri/ui"; +import { PlaygroundToolbarItem } from "@quri/squiggle-components"; +import { Button, LinkIcon, TextTooltip, useToast } from "@quri/ui"; import { EditSquiggleSnippetModel$key } from "@/__generated__/EditSquiggleSnippetModel.graphql"; import { @@ -17,6 +14,12 @@ import { EditModelExports } from "@/components/exports/EditModelExports"; import { useAsyncMutation } from "@/hooks/useAsyncMutation"; import { useAvailableHeight } from "@/hooks/useAvailableHeight"; import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; +import { + SquigglePlaygroundVersionPicker, + VersionedSquigglePlayground, + type SquiggleVersion, + SquiggleVersionShower, +} from "@quri/versioned-playground"; export const Mutation = graphql` mutation EditSquiggleSnippetModelMutation( @@ -67,6 +70,7 @@ export const EditSquiggleSnippetModel: FC = ({ modelRef }) => { ... on SquiggleSnippet { id code + version } } @@ -111,6 +115,9 @@ export const EditSquiggleSnippetModel: FC = ({ modelRef }) => { defaultValues: initialFormValues, }); + // could version picker be part of the form? + const [version, setVersion] = useState(content.version); + const { fields: variablesWithDefinitionsFields, append: appendVariableWithDefinition, @@ -134,6 +141,7 @@ export const EditSquiggleSnippetModel: FC = ({ modelRef }) => { input: { content: { code: formData.code, + version, }, relativeValuesExports: formData.relativeValuesExports, slug: model.slug, @@ -148,22 +156,53 @@ export const EditSquiggleSnippetModel: FC = ({ modelRef }) => { form.setValue("code", code); }; + // We don't want to control SquigglePlayground, it's uncontrolled by design. + // Instead, we reset the `defaultCode` that we pass to it when version is changed. + const [defaultCode, setDefaultCode] = useState(content.code); + + const handleVersionChange = (newVersion: SquiggleVersion) => { + setVersion(newVersion); + setDefaultCode(form.getValues("code")); + }; + return (
- - model.isEditable && ( -
+ defaultCode={defaultCode} + renderExtraControls={({ openModal }) => ( +
+ {model.isEditable && ( openModal("exports")} - > + /> + )} + {model.isEditable ? ( + + ) : ( + + {/* div wrapper is required because TextTooltip clones its children and SquiggleVersionShower doesn't forwardRef */} +
+ +
+
+ )} + {model.isEditable && ( -
- ) - } + )} +
+ )} renderExtraModal={(name) => { if (name === "exports") { return { diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx index f3141ebd48..686a582d8a 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx @@ -16,7 +16,7 @@ import { import { ModelLayoutQuery } from "@/__generated__/ModelLayoutQuery.graphql"; import { EntityLayout } from "@/components/EntityLayout"; -import { DropdownMenuLinkItem } from "@/components/ui/DropdownMenuLinkItem"; +import { DropdownMenuNextLinkItem } from "@/components/ui/DropdownMenuNextLinkItem"; import { EntityTab } from "@/components/ui/EntityTab"; import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; @@ -87,7 +87,7 @@ export const ModelLayout: FC< Relative Value Functions {model.currentRevision.relativeValuesExports.map((exportItem) => ( -
Version from{" "} - {format(model.revision.createdAtTimestamp, commonDateFormat)} + {format(model.revision.createdAtTimestamp, commonDateFormat)}.{" "} + Squiggle{" "} + {model.revision.content.version}.
Go to latest version
- + ); }; diff --git a/packages/hub/src/app/new/model/NewModel.tsx b/packages/hub/src/app/new/model/NewModel.tsx index b4cf278aae..1f6dee82b7 100644 --- a/packages/hub/src/app/new/model/NewModel.tsx +++ b/packages/hub/src/app/new/model/NewModel.tsx @@ -6,6 +6,7 @@ import { FormProvider, useForm } from "react-hook-form"; import { graphql } from "relay-runtime"; import { Button, CheckboxFormField } from "@quri/ui"; +import { defaultSquiggleVersion } from "@quri/versioned-playground"; import { NewModelMutation } from "@/__generated__/NewModelMutation.graphql"; import { SelectGroup, SelectGroupOption } from "@/components/SelectGroup"; @@ -75,6 +76,7 @@ export const NewModel: FC = () => { groupSlug: data.group?.slug, isPrivate: data.isPrivate, code: defaultCode, + version: defaultSquiggleVersion, }, }, onCompleted: (result) => { @@ -97,7 +99,7 @@ export const NewModel: FC = () => {

New Model

-
+
name="slug" example="my-long-model" diff --git a/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx b/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx index c5e39aea69..c25bd4a906 100644 --- a/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx @@ -1,8 +1,8 @@ import Link from "next/link"; import { FC } from "react"; +import { DropdownMenuNextLinkItem } from "@/components/ui/DropdownMenuNextLinkItem"; import { IconProps } from "@/relative-values/components/ui/icons/Icon"; -import { DropdownMenuLinkItem } from "@/components/ui/DropdownMenuLinkItem"; import { EmptyIcon } from "@quri/ui"; export type MenuLinkModeProps = @@ -41,7 +41,7 @@ export const PageMenuLink: FC = ({ {title} ) : ( - void; @@ -30,7 +29,7 @@ export const UserControlsMenu: FC = ({ close, username, mode }) => { {mode === "desktop" ? "User Actions" : `@${username}`} - = ({ close, username, mode }) => { Experimental - - ; - close: () => void; -}; - -export const DropdownMenuLinkItem: FC = ({ - title, - href, - icon, - close, -}) => { - const router = useRouter(); - - const onClick = () => { - router.push(href); - close(); - }; - - return ; -}; diff --git a/packages/hub/src/components/ui/DropdownMenuNextLinkItem.tsx b/packages/hub/src/components/ui/DropdownMenuNextLinkItem.tsx new file mode 100644 index 0000000000..328415d05d --- /dev/null +++ b/packages/hub/src/components/ui/DropdownMenuNextLinkItem.tsx @@ -0,0 +1,24 @@ +import { FC } from "react"; +import Link from "next/link"; + +import { DropdownMenuItemLayout, IconProps } from "@quri/ui"; + +type Props = { + href: string; + title: string; + icon?: FC; + close: () => void; +}; + +export const DropdownMenuNextLinkItem: FC = ({ + title, + href, + icon, + close, +}) => { + return ( + + + + ); +}; diff --git a/packages/hub/src/components/ui/MutationModalAction.tsx b/packages/hub/src/components/ui/MutationModalAction.tsx index 29400ba8dc..ab682b6979 100644 --- a/packages/hub/src/components/ui/MutationModalAction.tsx +++ b/packages/hub/src/components/ui/MutationModalAction.tsx @@ -14,6 +14,7 @@ import { CommonMutationParameters, useAsyncMutation, } from "@/hooks/useAsyncMutation"; +import { DropdownMenuModalActionItem } from "@quri/ui"; type CommonProps< TMutation extends CommonMutationParameters, @@ -100,25 +101,19 @@ export function MutationModalAction< icon?: FC; children: () => ReactNode; }): ReactNode { - const [isOpen, setIsOpen] = useState(false); - return ( - <> - setIsOpen(true)} - icon={icon} - /> - {isOpen && ( + ( // Note that we pass the same `close` that's responsible for closing the dropdown. - // There's no need to call setIsOpen(false); closing the dropdown is enough, since this component will be destroyed. {...modalProps} title={modalTitle} > {children()} )} - + /> ); } diff --git a/packages/hub/src/graphql/mutations/createSquiggleSnippetModel.ts b/packages/hub/src/graphql/mutations/createSquiggleSnippetModel.ts index 3aeb5727e1..580e8f9b05 100644 --- a/packages/hub/src/graphql/mutations/createSquiggleSnippetModel.ts +++ b/packages/hub/src/graphql/mutations/createSquiggleSnippetModel.ts @@ -25,6 +25,7 @@ builder.mutationField("createSquiggleSnippetModel", (t) => required: true, description: "Squiggle source code", }), + version: t.input.string({ required: true }), slug: t.input.string({ required: true, validate: validateSlug, @@ -60,6 +61,7 @@ builder.mutationField("createSquiggleSnippetModel", (t) => squiggleSnippet: { create: { code: input.code, + version: input.version, }, }, contentType: "SquiggleSnippet", diff --git a/packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts b/packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts index e5e284d782..574eda21ee 100644 --- a/packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts +++ b/packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts @@ -3,6 +3,7 @@ import { RelativeValuesDefinition } from "@prisma/client"; import { builder } from "@/graphql/builder"; import { prisma } from "@/prisma"; +import { squiggleVersions } from "@quri/versioned-playground"; import { Model, getWriteableModel } from "../types/Model"; const DefinitionRefInput = builder.inputType("DefinitionRefInput", { @@ -30,6 +31,7 @@ const SquiggleSnippetContentInput = builder.inputType( { fields: (t) => ({ code: t.string({ required: true }), + version: t.string({ required: true }), }), } ); @@ -48,10 +50,9 @@ builder.mutationField("updateSquiggleSnippetModel", (t) => relativeValuesExports: t.input.field({ type: [RelativeValuesExportInput], }), - code: t.input.string({ deprecationReason: "Use content arg instead" }), content: t.input.field({ type: SquiggleSnippetContentInput, - // TODO - should be required after `code` input is removed + required: true, }), }, resolve: async (_, { input }, { session }) => { @@ -61,10 +62,9 @@ builder.mutationField("updateSquiggleSnippetModel", (t) => owner: input.owner, }); - const code = input.code ?? input.content?.code; - if (code === undefined) { - // remove this after `code` support is removed - throw new Error("One of `code` and `content.code` must be set"); + const version = input.content.version; + if (!(squiggleVersions as readonly string[]).includes(version)) { + throw new Error(`Unknown Squiggle version ${version}`); } const relativeValuesExports = input.relativeValuesExports ?? []; @@ -122,7 +122,10 @@ builder.mutationField("updateSquiggleSnippetModel", (t) => const revision = await tx.modelRevision.create({ data: { squiggleSnippet: { - create: { code }, + create: { + code: input.content.code, + version: input.content.version, + }, }, contentType: "SquiggleSnippet", model: { diff --git a/packages/hub/src/graphql/types/ModelRevision.ts b/packages/hub/src/graphql/types/ModelRevision.ts index db8c8a25f8..5757ac9c2e 100644 --- a/packages/hub/src/graphql/types/ModelRevision.ts +++ b/packages/hub/src/graphql/types/ModelRevision.ts @@ -6,6 +6,7 @@ export const SquiggleSnippet = builder.prismaNode("SquiggleSnippet", { id: { field: "id" }, fields: (t) => ({ code: t.exposeString("code"), + version: t.exposeString("version"), }), }); diff --git a/packages/ops/package.json b/packages/ops/package.json index e763cc361a..f0ac38af7b 100644 --- a/packages/ops/package.json +++ b/packages/ops/package.json @@ -2,18 +2,23 @@ "name": "@quri/ops", "version": "0.1.0", "private": true, + "type": "module", "scripts": { "build": "tsc", - "changeset-version": "pnpm run build && node ./dist/changeset-version.mjs" + "changeset-version": "node ./dist/scripts/changeset-version.js", + "publish-all": "node ./dist/scripts/publish-all.js", + "post-publish": "node ./dist/scripts/post-publish.js" }, "devDependencies": { "@changesets/get-github-info": "^0.5.2", "@changesets/types": "^5.2.1", + "@types/lodash": "^4.14.196", "@types/node": "^20.4.7", "tsx": "^3.12.7", "typescript": "^5.1.6" }, "dependencies": { + "lodash": "^4.17.21", "mdast-util-to-string": "^4.0.0", "remark-parse": "^10.0.2", "remark-stringify": "^10.0.3", diff --git a/packages/ops/src/constants.ts b/packages/ops/src/constants.ts new file mode 100644 index 0000000000..69605cf0c0 --- /dev/null +++ b/packages/ops/src/constants.ts @@ -0,0 +1,15 @@ +export const WEBSITE_CHANGELOG_ROOT = + "packages/website/src/pages/docs/Changelog"; + +// versions of all these packages should be synced thanks to `fixed` field in `.changeset/config.json` +export const PRIMARY_SQUIGGLE_PACKAGE_DIRS = [ + "packages/squiggle-lang", + "packages/components", + "packages/prettier-plugin", + "packages/vscode-ext", +]; + +export const VSCODE_PACKAGE_NAME = "vscode-squiggle"; + +export const VSCODE_EXTENSION_URL = + "https://marketplace.visualstudio.com/items?itemName=QURI.vscode-squiggle"; diff --git a/packages/ops/src/lib.ts b/packages/ops/src/lib.ts new file mode 100644 index 0000000000..9f6d7e729b --- /dev/null +++ b/packages/ops/src/lib.ts @@ -0,0 +1,25 @@ +import { exec as originalExec } from "node:child_process"; +import util from "node:util"; +import fs from "node:fs/promises"; + +export const exec = util.promisify(originalExec); + +export async function exists(f: string): Promise { + let exists = true; + await fs.stat(f).catch((err) => { + if (err.code === "ENOENT") exists = false; + }); + return exists; +} + +export type PackageInfo = { + version: string; + name: string; +}; + +export async function getPackageInfo(packageDir: string): Promise { + const packageJson = JSON.parse( + await fs.readFile(`${packageDir}/package.json`, "utf-8") + ); + return { version: packageJson.version, name: packageJson.name }; // TODO: zod +} diff --git a/packages/ops/scripts/changelog.ts b/packages/ops/src/scripts/changelog.cts similarity index 96% rename from packages/ops/scripts/changelog.ts rename to packages/ops/src/scripts/changelog.cts index 4728a4cbb9..258825973d 100644 --- a/packages/ops/scripts/changelog.ts +++ b/packages/ops/src/scripts/changelog.cts @@ -4,6 +4,7 @@ import type { ChangelogFunctions } from "@changesets/types"; import { getInfo, getInfoFromPullRequest } from "@changesets/get-github-info"; +// can't be moved to constants.ts because it's ESM and changesets requires this file to be CJS const REPO = "quantified-uncertainty/squiggle"; const changelogFunctions: ChangelogFunctions = { diff --git a/packages/ops/scripts/changeset-version.mts b/packages/ops/src/scripts/changeset-version.ts similarity index 81% rename from packages/ops/scripts/changeset-version.mts rename to packages/ops/src/scripts/changeset-version.ts index db0002c1b0..684ac170cc 100644 --- a/packages/ops/scripts/changeset-version.mts +++ b/packages/ops/src/scripts/changeset-version.ts @@ -2,23 +2,21 @@ import { exec as originalExec } from "node:child_process"; import { readFile, writeFile } from "node:fs/promises"; import util from "node:util"; +import { toString as mdastToString } from "mdast-util-to-string"; import remarkParse from "remark-parse"; import { type Root } from "remark-parse/lib/index.js"; import remarkStringify from "remark-stringify"; import { unified } from "unified"; -import { toString as mdastToString } from "mdast-util-to-string"; - -const exec = util.promisify(originalExec); -const WEBSITE_CHANGELOG_ROOT = "packages/website/src/pages/docs/Changelog"; +import { + PRIMARY_SQUIGGLE_PACKAGE_DIRS, + VSCODE_EXTENSION_URL, + VSCODE_PACKAGE_NAME, + WEBSITE_CHANGELOG_ROOT, +} from "../constants.js"; +import { PackageInfo, getPackageInfo } from "../lib.js"; -// versions of all these packages should be synced thanks to `fixed` field in `.changeset/config.json` -const WEBSITE_CHANGELOG_PACKAGES = [ - "@quri/squiggle-lang", - "@quri/squiggle-components", - "@quri/prettier-plugin-squiggle", - "vscode-squiggle", -]; +const exec = util.promisify(originalExec); async function getChangedPackages() { const { stdout } = await exec("git status -s"); @@ -76,18 +74,6 @@ function getChangelogEntry(changelog: string, version: string) { }; } -type PackageInfo = { - version: string; - name: string; -}; - -async function getPackageInfo(packageDir: string): Promise { - const packageJson = JSON.parse( - await readFile(`${packageDir}/package.json`, "utf-8") - ); - return { version: packageJson.version, name: packageJson.name }; // TODO: zod -} - type PackageChangelog = { packageDir: string; packageInfo: PackageInfo; @@ -111,15 +97,17 @@ function combineChangelogs(changelogs: PackageChangelog[]): { let content = `## ${version}\n\n`; - for (const packageName of WEBSITE_CHANGELOG_PACKAGES) { + for (const packageDir of PRIMARY_SQUIGGLE_PACKAGE_DIRS) { const changelog = changelogs.find( - (changelog) => changelog.packageInfo.name === packageName + (changelog) => changelog.packageInfo.name === packageDir ); if (!changelog) continue; + const packageName = changelog.packageInfo.name; + const link = - packageName === "vscode-squiggle" - ? "https://marketplace.visualstudio.com/items?itemName=QURI.vscode-squiggle" + packageName === VSCODE_PACKAGE_NAME + ? VSCODE_EXTENSION_URL : `https://www.npmjs.com/package/${packageName}`; content += `### [${packageName}](${link})\n\n`; @@ -153,7 +141,7 @@ async function generateWebsiteChangelog() { for (const packageDir of packageDirs) { const changelog = await generatePackageChanges(packageDir); - if (WEBSITE_CHANGELOG_PACKAGES.includes(changelog.packageInfo.name)) { + if (PRIMARY_SQUIGGLE_PACKAGE_DIRS.includes(packageDir)) { allChangelogs.push(changelog); } } diff --git a/packages/ops/scripts/cleanup-vercel-deployments.ts b/packages/ops/src/scripts/cleanup-vercel-deployments.ts similarity index 100% rename from packages/ops/scripts/cleanup-vercel-deployments.ts rename to packages/ops/src/scripts/cleanup-vercel-deployments.ts diff --git a/packages/ops/src/scripts/post-publish.ts b/packages/ops/src/scripts/post-publish.ts new file mode 100644 index 0000000000..f0e430218b --- /dev/null +++ b/packages/ops/src/scripts/post-publish.ts @@ -0,0 +1,101 @@ +import escapeRegExp from "lodash/escapeRegExp.js"; +import { readFile, writeFile } from "node:fs/promises"; + +import { PRIMARY_SQUIGGLE_PACKAGE_DIRS } from "../constants.js"; +import { PackageInfo, exec, exists, getPackageInfo } from "../lib.js"; + +async function insertVersionToVersionedPlayground(version: string) { + process.chdir("packages/versioned-playground"); + + const alias = `squiggle-components-${version}`; + await exec(`pnpm add ${alias}@npm:@quri/squiggle-components@${version}`); + + { + const componentFilename = "src/VersionedSquigglePlayground.tsx"; + let playgroundComponent = await readFile(componentFilename, "utf-8"); + + // new RegExp is important! not sure why, it's not necessary for match(), but necessary for replace(), JS is weird + const regex = new RegExp(escapeRegExp("dev: lazy(async () => ({")); + if (!playgroundComponent.match(regex)) { + throw new Error("Can't find lazy load declarations to patch"); + } + playgroundComponent = playgroundComponent.replace( + regex, + `"${version}": lazy(async () => ({ + default: (await import("${alias}")).SquigglePlayground, + })), + $&` + ); + await writeFile(componentFilename, playgroundComponent, "utf-8"); + } + + { + const versionsFilename = "src/versions.ts"; + let versionsCode = await readFile(versionsFilename, "utf-8"); + + const versionsRegex = new RegExp( + escapeRegExp("export const squiggleVersions = [") + ); + if (!versionsCode.match(versionsRegex)) { + throw new Error("Can't find versions string"); + } + versionsCode = versionsCode.replace(versionsRegex, `$&"${version}", `); + + const defaultVersionRegex = + /(export const defaultSquiggleVersion: SquiggleVersion = ")[^"]+/; + if (!versionsCode.match(defaultVersionRegex)) { + throw new Error("Can't find default version string"); + } + versionsCode = versionsCode.replace(defaultVersionRegex, `$1${version}`); + await writeFile(versionsFilename, versionsCode, "utf-8"); + } + + // TODO - run `pnpm format` to make sure we didn't break prettier? + + process.chdir("../.."); +} + +async function bumpVersionsToDev() { + for (const packageDir of PRIMARY_SQUIGGLE_PACKAGE_DIRS) { + process.chdir(packageDir); + await exec("pnpm version prerelease"); + process.chdir("../.."); + } +} + +// necessary to block the subsequent automatic release +async function createEmptyChangeset() { + const filename = ".changeset/next-release.md"; + if (await exists(filename)) { + throw new Error("next-release.md already exists"); + } + + const packages: PackageInfo[] = []; + for (const dir of PRIMARY_SQUIGGLE_PACKAGE_DIRS) { + packages.push(await getPackageInfo(dir)); + } + + const content = + "---\n" + + packages.map((packageInfo) => `"${packageInfo.name}": patch\n`).join("") + + "---\n"; + await writeFile(filename, content); +} + +async function main() { + process.chdir("../.."); + + // We have to do things in this order, because attempt to `pnpm add` a version to versioned-playground results in a local workspace: dependency + // so we cache an old version first, then bump all package versions, and only then update versioned-playground + const { version: releasedVersion } = await getPackageInfo( + PRIMARY_SQUIGGLE_PACKAGE_DIRS[0] + ); + await bumpVersionsToDev(); + await insertVersionToVersionedPlayground(releasedVersion); + + await exec("cd packages/squiggle-lang && pnpm run update-system-version"); + + await createEmptyChangeset(); +} + +main(); diff --git a/packages/ops/src/scripts/publish-all.ts b/packages/ops/src/scripts/publish-all.ts new file mode 100644 index 0000000000..825456b711 --- /dev/null +++ b/packages/ops/src/scripts/publish-all.ts @@ -0,0 +1,14 @@ +import { exec } from "../lib.js"; + +async function main() { + process.chdir("../.."); + await exec("npx turbo lint build"); + + await exec("npx changeset publish"); + process.chdir("packages/vscode-ext"); + await exec("pnpm run package"); + await exec("npx vsce publish --no-dependencies --skip-duplicate"); + process.chdir("../.."); +} + +main(); diff --git a/packages/ops/tsconfig.json b/packages/ops/tsconfig.json index 183c9edc02..5d10b56814 100644 --- a/packages/ops/tsconfig.json +++ b/packages/ops/tsconfig.json @@ -1,14 +1,14 @@ { "compilerOptions": { - "module": "Node16", // required by changeset for changelog.ts script + "module": "Node16", "moduleResolution": "Node16", "target": "es2021", "esModuleInterop": true, - "rootDir": "scripts", + "rootDir": "src", "outDir": "dist", // type check settings "strict": true, "skipLibCheck": true }, - "include": ["scripts/**/*.ts", "scripts/changeset-version.mts"] + "include": ["src/**/*.ts", "src/**/*.cts"] } diff --git a/packages/squiggle-lang/.gitignore b/packages/squiggle-lang/.gitignore index a0c0921139..f345636d4c 100644 --- a/packages/squiggle-lang/.gitignore +++ b/packages/squiggle-lang/.gitignore @@ -8,7 +8,6 @@ build yarn-error.log .idea /dist -/*.tsbuildinfo *.coverage _coverage coverage diff --git a/packages/squiggle-lang/src/library/version.ts b/packages/squiggle-lang/src/library/version.ts index 8258fb76fa..c8b5773fdf 100644 --- a/packages/squiggle-lang/src/library/version.ts +++ b/packages/squiggle-lang/src/library/version.ts @@ -2,6 +2,6 @@ import { ImmutableMap } from "../utility/immutableMap.js"; import { Value, vString } from "../value/index.js"; export function makeVersionConstant(): ImmutableMap { - // TODO - generate during build based on package.json + // automatically updated on release return ImmutableMap([["System.version", vString("0.8.5")]]); } diff --git a/packages/ui/.gitignore b/packages/ui/.gitignore index 0a639bceda..e0baa61095 100644 --- a/packages/ui/.gitignore +++ b/packages/ui/.gitignore @@ -1,3 +1,2 @@ /dist /storybook-static -/tsconfig.tsbuildinfo diff --git a/packages/ui/package.json b/packages/ui/package.json index 06eb9db52a..9325684b3f 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -53,14 +53,14 @@ "react-use": "^17.4.0" }, "devDependencies": { - "@storybook/addon-docs": "^7.2.1", - "@storybook/addon-essentials": "^7.2.1", - "@storybook/addon-interactions": "^7.2.1", - "@storybook/addon-links": "^7.2.1", - "@storybook/blocks": "^7.4.0", - "@storybook/react": "^7.0.24", - "@storybook/react-vite": "^7.2.1", - "@storybook/testing-library": "^0.2.0", + "@storybook/addon-docs": "^7.4.5", + "@storybook/addon-essentials": "^7.4.5", + "@storybook/addon-interactions": "^7.4.5", + "@storybook/addon-links": "^7.4.5", + "@storybook/blocks": "^7.4.5", + "@storybook/react": "^7.4.5", + "@storybook/react-vite": "^7.4.5", + "@storybook/testing-library": "^0.2.1", "@tailwindcss/forms": "^0.5.6", "@types/react": "^18.2.18", "@types/react-dom": "^18.2.7", diff --git a/packages/ui/src/components/Dropdown/DropdownMenuActionItem.tsx b/packages/ui/src/components/Dropdown/DropdownMenuActionItem.tsx index c560a8e145..25908eddc4 100644 --- a/packages/ui/src/components/Dropdown/DropdownMenuActionItem.tsx +++ b/packages/ui/src/components/Dropdown/DropdownMenuActionItem.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; import { - ActionItemInternal, - ActionItemInternalProps, -} from "./DropdownMenuActionItemInternal.js"; + DropdownMenuItemLayout, + ItemLayoutProps, +} from "./DropdownMenuItemLayout.js"; -type ActionItemProps = ActionItemInternalProps & { +type ActionItemProps = ItemLayoutProps & { onClick(): void; }; @@ -15,7 +15,7 @@ export const DropdownMenuActionItem: FC = ({ }) => { return (
- +
); }; diff --git a/packages/ui/src/components/Dropdown/DropdownMenuAsyncActionItem.tsx b/packages/ui/src/components/Dropdown/DropdownMenuAsyncActionItem.tsx index 301a8ea4a3..8fefa4a0d3 100644 --- a/packages/ui/src/components/Dropdown/DropdownMenuAsyncActionItem.tsx +++ b/packages/ui/src/components/Dropdown/DropdownMenuAsyncActionItem.tsx @@ -2,7 +2,7 @@ import { FC, useState } from "react"; import { IconProps } from "../../icons/Icon.js"; -import { ActionItemInternal } from "./DropdownMenuActionItemInternal.js"; +import { DropdownMenuItemLayout } from "./DropdownMenuItemLayout.js"; type AsyncActionItemProps = { icon?: FC; @@ -30,7 +30,7 @@ export const DropdownMenuAsyncActionItem: FC = ({ return (
- +
); }; diff --git a/packages/ui/src/components/Dropdown/DropdownMenuActionItemInternal.tsx b/packages/ui/src/components/Dropdown/DropdownMenuItemLayout.tsx similarity index 89% rename from packages/ui/src/components/Dropdown/DropdownMenuActionItemInternal.tsx rename to packages/ui/src/components/Dropdown/DropdownMenuItemLayout.tsx index 28619ae519..a87ef4fd97 100644 --- a/packages/ui/src/components/Dropdown/DropdownMenuActionItemInternal.tsx +++ b/packages/ui/src/components/Dropdown/DropdownMenuItemLayout.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { RefreshIcon } from "../../icons/RefreshIcon.js"; import { IconProps } from "../../icons/Icon.js"; -export type ActionItemInternalProps = { +export type ItemLayoutProps = { icon?: FC; title: string; acting?: boolean; @@ -24,7 +24,7 @@ const iconDisplay = (icon?: FC, acting?: boolean) => { ); }; -export const ActionItemInternal: FC = ({ +export const DropdownMenuItemLayout: FC = ({ title, icon, acting, diff --git a/packages/ui/src/components/Dropdown/DropdownMenuLinkItem.tsx b/packages/ui/src/components/Dropdown/DropdownMenuLinkItem.tsx new file mode 100644 index 0000000000..ea13576c0b --- /dev/null +++ b/packages/ui/src/components/Dropdown/DropdownMenuLinkItem.tsx @@ -0,0 +1,33 @@ +import { FC } from "react"; +import { IconProps } from "../../icons/Icon.js"; +import { DropdownMenuItemLayout } from "./DropdownMenuItemLayout.js"; +import { ExternalLinkIcon } from "../../index.js"; + +type Props = { + href: string; + title: string; + icon?: FC; + close: () => void; + newTab?: boolean; +}; + +// In Next.js apps you should prefer `DropdownMenuNextLinkItem` instead of using this component. +// (See hub's source code for implementation.) +export const DropdownMenuLinkItem: FC = ({ + href, + title, + icon, + close, + newTab, +}) => { + return ( +
+ + + ); +}; diff --git a/packages/ui/src/components/Dropdown/DropdownMenuModalActionItem.tsx b/packages/ui/src/components/Dropdown/DropdownMenuModalActionItem.tsx new file mode 100644 index 0000000000..0750fda4d7 --- /dev/null +++ b/packages/ui/src/components/Dropdown/DropdownMenuModalActionItem.tsx @@ -0,0 +1,31 @@ +"use client"; +import { FC, ReactNode, useState } from "react"; +import { ItemLayoutProps } from "./DropdownMenuItemLayout.js"; +import { DropdownMenuActionItem } from "./DropdownMenuActionItem.js"; + +type Props = ItemLayoutProps & { + render(): ReactNode; +}; + +/* + * This component doesn't close the dropdown when modal is displayed. + * Instead, should close the dropdown manually by passing Dropdown's `close()` to Modal's `close` prop. + */ +export const DropdownMenuModalActionItem: FC = ({ + title, + icon, + render, +}) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + setIsOpen(true)} + icon={icon} + /> + {isOpen && render()} + + ); +}; diff --git a/packages/ui/src/forms/common/ControlledFormField.tsx b/packages/ui/src/forms/common/ControlledFormField.tsx index 8ee569cc82..8e9d77e179 100644 --- a/packages/ui/src/forms/common/ControlledFormField.tsx +++ b/packages/ui/src/forms/common/ControlledFormField.tsx @@ -37,6 +37,7 @@ export function ControlledFormField< label, description, inlineLabel, + standaloneLabel, children, }: ControlledFormFieldProps) { return ( @@ -44,6 +45,7 @@ export function ControlledFormField< label={label} description={description} inlineLabel={inlineLabel} + standaloneLabel={standaloneLabel} > {children} diff --git a/packages/ui/src/forms/common/FormField.tsx b/packages/ui/src/forms/common/FormField.tsx index fa3056a5a0..63e3add53d 100644 --- a/packages/ui/src/forms/common/FormField.tsx +++ b/packages/ui/src/forms/common/FormField.tsx @@ -27,6 +27,7 @@ export function FormField< label, description, inlineLabel, + standaloneLabel, children, }: FormFieldProps) { return ( @@ -34,6 +35,7 @@ export function FormField< label={label} description={description} inlineLabel={inlineLabel} + standaloneLabel={standaloneLabel} > {children} diff --git a/packages/ui/src/forms/common/FormFieldLayout.tsx b/packages/ui/src/forms/common/FormFieldLayout.tsx index 99b4996446..8e60a23d4c 100644 --- a/packages/ui/src/forms/common/FormFieldLayout.tsx +++ b/packages/ui/src/forms/common/FormFieldLayout.tsx @@ -2,27 +2,35 @@ import { clsx } from "clsx"; import { FC, PropsWithChildren } from "react"; export type FormFieldLayoutProps = { - label?: string; // TODO - `inlineLabel` prop for checkboxes and color inputs - description?: string; // TODO - allow ReactNode for inline links and other formatting? - inlineLabel?: boolean; // useful for checkboxes + label?: string; + // TODO - allow ReactNode for inline links and other formatting? + description?: string; + // useful for checkboxes + inlineLabel?: boolean; + // If set, label won't wrap children and won't be clickable. This is useful for some custom fields where outer label leads to too large clickable area. + standaloneLabel?: boolean; }; export const FieldLayout: FC> = ({ label, - inlineLabel, description, + inlineLabel, + standaloneLabel, children, }) => { + const OuterTag = standaloneLabel ? "div" : "label"; + const InnerTag = standaloneLabel ? "label" : "div"; + // TODO - use