refactor: Centralize Builder ID generation (#6323)

## Outcome

Builder-owned code now routes standard ID generation through
`@webstudio-is/sdk/createId` while preserving existing NanoID and UUID
formats.

Closes #6322

## Technical choices

- Use `createId("nano")` for Builder records, assets, transaction IDs,
and other existing NanoID-backed values.
- Use `createId()` for UUID-backed database records and browser-side
client IDs.
- Keep `BuilderRuntimeContext.createId` as an injectable dependency.
Runtime operations, copy/paste, and deterministic tests rely on that
injection.
- Keep Node's `randomUUID` in CLI paths that explicitly support running
without the Web Crypto global.
- Keep specialized generators for custom-alphabet project domains and
actual hash generation.
- Remove direct `nanoid` dependencies from packages where no specialized
use remains.
- Do not migrate existing data or change database schemas.

## Todo

- [x] Migrate Builder app ID creation.
- [x] Migrate project runtime and template defaults.
- [x] Migrate asset, project, domain, and authorization-token IDs.
- [x] Preserve runtime ID injection.
- [x] Review remaining direct generators as explicit exceptions.
- [x] Add NanoID-format regression coverage for the default runtime
context.
- [x] Review documentation impact: no user-facing documentation changes
are needed.
- [x] Review fixture impact: no fixture input or generated output format
changes are needed.
- [x] Run targeted tests, typechecks, lint, and package-boundary checks.

## Verification

- Targeted Builder tests: 153 passed after preserving runtime-context
injection.
- Project-build runtime tests: 270 passed.
- Asset uploader tests: 61 passed.
- Authorization-token tests: 17 passed.
- Project tests: 52 passed.
- Domain tests: 18 passed.
- CLI tests: 32 passed.
- Typechecks passed for Builder, project-build, asset-uploader,
authorization-token, project, and domain.
- `pnpm lint` passed.
- `pnpm check:package-boundaries` passed.
- The runtime-context format test was demonstrated to fail with a UUID
default and pass with the NanoID default.
- Agent evaluations were not run because they require separate explicit
approval.
This commit is contained in:
Oleg Isonen authored and GitHub committed 2026-09-03 15:56:23 +02:00
1 parent 7ff42abfb5
commit b832a8e808
47 files changed
+162 -147

No files matched your search

@@ -1,6 +1,5 @@
import { Fragment, useState, useMemo } from "react";
import { nanoid } from "nanoid";
import type { Breakpoint } from "@webstudio-is/sdk";
import { createId, type Breakpoint } from "@webstudio-is/sdk";
import {
theme,
Flex,
@@ -300,7 +299,7 @@ export const BreakpointsEditor = ({
return;
}
const newBreakpoint: Breakpoint = {
id: nanoid(),
id: createId("nano"),
label: "",
minWidth: 0,
};
@@ -1,6 +1,6 @@
import { Select, toast } from "@webstudio-is/design-system";
import { nanoid } from "nanoid";
import { useState } from "react";
import { createId, type AnimationAction } from "@webstudio-is/sdk";
import {
$hoveredInstanceSelector,
$registeredComponentMetas,
@@ -10,7 +10,6 @@ import { $instances } from "~/shared/sync/data-stores";
import { getInstanceStyleDecl } from "~/builder/features/style-panel/shared/model";
import { executeRuntimeMutation } from "~/shared/instance-utils/data";
import { toValue } from "@webstudio-is/css-engine";
import type { AnimationAction } from "@webstudio-is/sdk";
import { getInstanceLabel } from "~/builder/shared/instance-label";
const initSubjects = () => {
@@ -57,7 +56,7 @@ const initSubjects = () => {
const isTimelineExists = viewTimelineName.startsWith("--");
const value = isTimelineExists
? viewTimelineName
: `--generated-timeline-${nanoid()}`;
: `--generated-timeline-${createId("nano")}`;
subjects.push({
value,
@@ -1,9 +1,9 @@
import { nanoid } from "nanoid";
import { useState } from "react";
import { computed } from "nanostores";
import { useStore } from "@nanostores/react";
import { matchSorter } from "match-sorter";
import {
createId,
type Instance,
type Prop,
type Props,
@@ -378,7 +378,7 @@ export const PropsSection = (props: PropsSectionProps) => {
if (isEphemeral && value !== undefined) {
memoryInstanceProp.set(animationAction.propName, {
id: nanoid(),
id: createId("nano"),
instanceId: props.instanceId,
type: "animationAction",
name: animationAction.propName,
@@ -1,4 +1,4 @@
import { nanoid } from "nanoid";
import { createId } from "@webstudio-is/sdk";
import { useState } from "react";
import { Flex, StorySection, Text, theme } from "@webstudio-is/design-system";
import {
@@ -7,6 +7,8 @@ import {
StyleSourceInput as StyleSourceInputComponent,
} from ".";
const createNanoId = () => createId("nano");
export default {
title: "Style panel/Style Source Input",
component: StyleSourceInputComponent,
@@ -22,7 +24,7 @@ type Item = {
};
const localItem: Item = {
id: nanoid(),
id: createNanoId(),
label: "Local",
source: "local",
disabled: false,
@@ -32,7 +34,7 @@ const localItem: Item = {
const getItems = (): Array<Item> => [
{
id: nanoid(),
id: createNanoId(),
label: "Token",
source: "token",
disabled: false,
@@ -40,7 +42,7 @@ const getItems = (): Array<Item> => [
states: [],
},
{
id: nanoid(),
id: createNanoId(),
label: "Tag",
source: "tag",
disabled: false,
@@ -55,7 +57,7 @@ const createItem = (
setValue: (value: Array<Item>) => void
) => {
const item: Item = {
id: nanoid(),
id: createNanoId(),
label,
source: "token",
disabled: false,
@@ -78,7 +80,7 @@ export const StyleSourceInput = () => {
localItem,
...getItems(),
{
id: nanoid(),
id: createNanoId(),
label: "Disabled",
source: "token",
disabled: true,
@@ -91,7 +93,7 @@ export const StyleSourceInput = () => {
>({ styleSourceId: localItem.id });
const [editingItemId, setEditingItemId] = useState<undefined | Item["id"]>();
const truncatedId = nanoid();
const truncatedId = createNanoId();
const [truncated, setTruncated] = useState<Array<Item>>([
{
id: truncatedId,
@@ -5,8 +5,8 @@ import type {
VideoAsset,
AllowedFileExtension,
} from "@webstudio-is/sdk";
import { nanoid } from "nanoid";
import {
createId,
getMimeTypeByExtension,
getFileExtension,
IMAGE_EXTENSIONS,
@@ -51,7 +51,7 @@ const extractImageNameAndMimeTypeFromUrl = (url: URL) => {
// Any image format is suitable
const FALLBACK_URL_TYPE = "image/png";
return [FALLBACK_URL_TYPE, `${nanoid()}.png`] as const;
return [FALLBACK_URL_TYPE, `${createId("nano")}.png`] as const;
};
export const getSha256Hash = async (data: string) => {
@@ -50,9 +50,13 @@ import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
import { nanoid } from "nanoid";
import { createRegularStyleSheet } from "@webstudio-is/css-engine";
import type { Instance, Instances, Props } from "@webstudio-is/sdk";
import {
createId,
type Instance,
type Instances,
type Props,
} from "@webstudio-is/sdk";
import {
inflatedAttribute,
idAttribute,
@@ -149,7 +153,7 @@ const BindInstanceToNodePlugin = ({
*/
const CaretColorPlugin = () => {
const [editor] = useLexicalComposerContext();
const caretClassName = useState(() => `a${nanoid()}`)[0];
const caretClassName = useState(() => `a${createId("nano")}`)[0];
useEffect(() => {
const rootElement = editor.getRootElement();
@@ -1552,8 +1556,8 @@ export const TextEditor = ({
}: TextEditorProps) => {
const [rootInstanceSelector] = useState(() => rootInstanceSelectorUnstable);
// class names must be started with letter so we add a prefix
const [paragraphClassName] = useState(() => `a${nanoid()}`);
const [italicClassName] = useState(() => `a${nanoid()}`);
const [paragraphClassName] = useState(() => `a${createId("nano")}`);
const [italicClassName] = useState(() => `a${createId("nano")}`);
const lastSavedStateJsonRef = useRef<SerializedEditorState | null>(null);
const [newLinkKeyToInstanceId] = useState(() => new Map());
+4 -2
View File
@@ -1,5 +1,6 @@
import { useRevalidator, useSearchParams } from "react-router-dom";
import { useState, type ComponentProps } from "react";
import { createId } from "@webstudio-is/sdk";
import {
Text,
theme,
@@ -28,7 +29,6 @@ import {
} from "@webstudio-is/design-system";
import { nativeClient } from "~/shared/trpc/trpc-client";
import type { User } from "~/shared/db/user.server";
import { nanoid } from "nanoid";
import { EllipsesIcon, SpinnerIcon } from "@webstudio-is/icons";
const tagColorPalette = Array.from({ length: 50 }, (_, index) => {
@@ -322,7 +322,9 @@ export const TagsDialog = ({
<DialogActions>
<Button
color="primary"
onClick={() => setEditingTag({ id: nanoid(5), label: "" })}
onClick={() =>
setEditingTag({ id: createId("nano"), label: "" })
}
>
Create tag
</Button>
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { createId } from "@webstudio-is/sdk";
import { useRevalidator } from "@remix-run/react";
import {
Button,
@@ -491,7 +492,7 @@ export const ManageMembersDialog = ({
}
if (succeeded.length > 0) {
const optimistic: OptimisticPendingInvite[] = succeeded.map((email) => ({
notificationId: crypto.randomUUID(),
notificationId: createId(),
email,
relation,
}));
@@ -1,5 +1,5 @@
import { nanoid } from "nanoid";
import { z } from "zod";
import { createId } from "@webstudio-is/sdk";
import { loadById, patchBuild } from "@webstudio-is/project/index.server";
import type { CompactBuild } from "@webstudio-is/project-build";
import {
@@ -193,6 +193,6 @@ export const commitBuildPatch = async ({
projectId,
buildId: build.id,
clientVersion: build.version,
transactions: [{ id: nanoid(), payload }],
transactions: [{ id: createId("nano"), payload }],
});
};
@@ -31,6 +31,7 @@ import {
type ProjectBundle,
} from "@webstudio-is/protocol";
import {
createId,
getHomePage,
normalizeAssetFolderData,
assetFolders as assetFoldersSchema,
@@ -340,7 +341,7 @@ export const importPublishedProjectBundle = async (
const buildUpdate = createBuildImportUpdate({
data,
lastTransactionId: crypto.randomUUID(),
lastTransactionId: createId(),
updatedAt: new Date().toISOString(),
version: nextVersion,
});
@@ -1,6 +1,6 @@
import type { JSX } from "react";
import { test, expect, describe, beforeEach } from "vitest";
import { nanoid } from "nanoid";
import { createId } from "@webstudio-is/sdk";
import {
type NestingRule,
createRegularStyleSheet,
@@ -109,7 +109,7 @@ beforeEach(() => {
$breakpoints.set(
new Map(
initialBreakpoints.map((breakpoint) => {
const id = nanoid();
const id = createId("nano");
return [id, { ...breakpoint, id }];
})
)
+2 -1
View File
@@ -1,4 +1,5 @@
import type { Database } from "@webstudio-is/postgrest/index.server";
import { createId } from "@webstudio-is/sdk";
import {
AuthorizationError,
type AppContext,
@@ -86,7 +87,7 @@ const genericCreateAccount = async (
throw new Error("User not found");
}
const userId = crypto.randomUUID();
const userId = createId();
const newUser = await context.postgrest.client
.from("User")
@@ -1,4 +1,3 @@
import { nanoid } from "nanoid";
import { shallowEqual } from "shallow-equal";
import type { ExoticComponent } from "react";
import { atom } from "nanostores";
@@ -9,6 +8,7 @@ import type {
InstanceData,
} from "@webstudio-is/react-sdk";
import {
createId,
getIndexesWithinAncestors,
type Instance,
type WsComponentMeta,
@@ -60,7 +60,7 @@ const createHookContext = (): HookContext => {
const newProps = props.get(instanceKey) ?? new Map();
const propBase = {
id: nanoid(),
id: createId("nano"),
instanceId: instanceData.id,
name: propName,
};
+9 -9
View File
@@ -4,7 +4,6 @@ import {
$workspaceRole,
$workspaces,
} from "~/dashboard/workspace/workspace-stores";
import { nanoid } from "nanoid";
import type { AuthPermit } from "@webstudio-is/trpc-interface/index.server";
import {
defaultPlanFeatures,
@@ -14,13 +13,14 @@ import {
import type { Role } from "@webstudio-is/project";
import type { User } from "~/shared/db/user.server";
import { toast, type Placement } from "@webstudio-is/design-system";
import type {
Instance,
Prop,
Props,
StyleDecl,
StyleSource,
AssetType,
import {
createId,
type AssetType,
type Instance,
type Prop,
type Props,
type StyleDecl,
type StyleSource,
} from "@webstudio-is/sdk";
import type { CssProperty, UnitValue } from "@webstudio-is/css-engine";
import type { TokenPermissions } from "@webstudio-is/authorization-token";
@@ -233,7 +233,7 @@ export const $selectedInstanceStyleSources = computed(
// always put local style source last
selectedInstanceStyleSources.push({
type: "local",
id: nanoid(),
id: createId("nano"),
});
}
return selectedInstanceStyleSources;
@@ -22,6 +22,7 @@ import {
type Subscription,
} from "./polling-manager";
import type { TopicName, TopicRegistry, SubscriptionResponse } from "./types";
import { createId } from "@webstudio-is/sdk";
// ── Channel message types ────────────────────────────────────────
@@ -82,7 +83,7 @@ export const createCrossTabPollingManager = (
channelName = DEFAULT_CHANNEL_NAME,
heartbeatInterval = HEARTBEAT_INTERVAL,
heartbeatTimeout = HEARTBEAT_TIMEOUT,
tabId = crypto.randomUUID(),
tabId = createId(),
createChannel = (name: string) => new BroadcastChannel(name),
...managerOptions
} = options;
@@ -7,7 +7,7 @@ import {
StorySection,
} from "@webstudio-is/design-system";
import { useEffect, useState } from "react";
import { nanoid } from "nanoid";
import { createId } from "@webstudio-is/sdk";
import {
type LinkOptions,
ShareProject as ShareProjectComponent,
@@ -20,7 +20,7 @@ export default {
const initialLinks: Array<LinkOptions> = [
{
token: nanoid(),
token: createId("nano"),
name: "View Only",
relation: "viewers",
canClone: false,
@@ -29,7 +29,7 @@ const initialLinks: Array<LinkOptions> = [
canUseApi: false,
},
{
token: nanoid(),
token: createId("nano"),
name: "View and Edit",
relation: "editors",
canClone: false,
@@ -38,7 +38,7 @@ const initialLinks: Array<LinkOptions> = [
canUseApi: false,
},
{
token: nanoid(),
token: createId("nano"),
name: "Build",
relation: "builders",
canClone: false,
@@ -70,7 +70,7 @@ const useShareProject = (
setLinks([
...currentLinks,
{
token: nanoid(),
token: createId("nano"),
name: "Custom Link",
relation: "viewers",
canClone: false,
+7 -3
View File
@@ -1,5 +1,5 @@
import { nanoid } from "nanoid";
import { createNanoEvents } from "nanoevents";
import { createId } from "@webstudio-is/sdk";
import type { Change, Store } from "immerhin";
import type { WritableAtom } from "nanostores";
import type {
@@ -154,7 +154,11 @@ export class NanostoresSyncObject implements SyncObject {
if (this.operation !== "local") {
return;
}
const transaction = { id: nanoid(), object: this.name, payload };
const transaction = {
id: createId("nano"),
object: this.name,
payload,
};
sendTransaction(transaction);
});
signal.addEventListener("abort", unsubscribe);
@@ -218,7 +222,7 @@ type SyncClientOptions = {
};
export class SyncClient {
clientId = nanoid();
clientId = createId("nano");
role: SyncClientOptions["role"];
object: SyncObject;
storages: SyncStorage[];
@@ -1,4 +1,5 @@
import type { Project } from "@webstudio-is/project";
import { createId } from "@webstudio-is/sdk";
import type { AuthPermit } from "@webstudio-is/trpc-interface/index.server";
import { toast } from "@webstudio-is/design-system";
import { nativeClient } from "~/shared/trpc/trpc-client";
@@ -64,7 +65,7 @@ export const createMultiplayerSyncClient = ({
}): MultiplayerSyncClient => {
let buildId: string | undefined;
let unsubscribePresence: (() => void) | undefined;
const clientId = crypto.randomUUID();
const clientId = createId();
const emitter = createMultiplayerSyncEmitter({
getAuthToken: createCollabAuthTokenLoader({
authPermit,
+6 -2
View File
@@ -2,7 +2,7 @@ import { Store } from "immerhin";
import { enableMapSet, setAutoFreeze } from "immer";
import { useEffect } from "react";
import { batched } from "nanostores";
import { nanoid } from "nanoid";
import { createId } from "@webstudio-is/sdk";
import { $project } from "./data-stores";
import {
$selectedPageHash,
@@ -243,7 +243,11 @@ class SelectedPageAndInstanceSyncObject {
) {
return;
}
sendTransaction({ id: nanoid(), object: this.name, payload: state });
sendTransaction({
id: createId("nano"),
object: this.name,
payload: state,
});
}
);
signal.addEventListener("abort", unsubscribe);
-1
View File
@@ -20,7 +20,6 @@
"fontkit": "^2.0.4",
"image-meta": "^0.2.1",
"immer": "^10.1.1",
"nanoid": "^5.1.5",
"warn-once": "^0.1.1"
},
"devDependencies": {
@@ -41,6 +41,7 @@ import {
type ContentSource,
} from "@webstudio-is/content-engine/compiler";
import {
createId,
toAssetReferenceRuntimeData,
type Asset,
type AssetFolder,
@@ -62,7 +63,6 @@ import type {
AssetObjectStore,
AssetObjectWriter,
} from "./client";
import { nanoid } from "nanoid";
import {
createUploadTicket,
uploadFile,
@@ -155,7 +155,7 @@ const defaultDependencies = {
loadAssetFoldersByProjectWithClient,
upsertAssetFolderWithClient,
deleteAssetFoldersWithClient,
createId: (): string => nanoid(),
createId: (): string => createId("nano"),
now: () => new Date(),
loadCanonicalAssetBaseEntries,
synchronizeCanonicalAssets,
+2 -2
View File
@@ -5,8 +5,8 @@ import {
getProjectPlanFeatures,
} from "@webstudio-is/trpc-interface/index.server";
import { createHash } from "node:crypto";
import { nanoid } from "nanoid";
import {
createId as createSdkId,
getFileExtension,
getFileNameParts,
type Asset,
@@ -287,7 +287,7 @@ const cleanupUploadError = async (
export const createUploadTicket = async (
data: CreateUploadTicketInput,
context: AppContext,
createId: () => Asset["id"] = nanoid
createId: () => Asset["id"] = () => createSdkId("nano")
): Promise<UploadTicket> => {
const {
projectId,
@@ -1,7 +1,6 @@
import { nanoid } from "nanoid";
import { getFileNameParts } from "@webstudio-is/sdk";
import { createId, getFileNameParts } from "@webstudio-is/sdk";
export const createUniqueAssetFilename = (filename: string) => {
const { basename, extension } = getFileNameParts(filename);
return `${basename}_${nanoid()}${extension === "" ? "" : `.${extension}`}`;
return `${basename}_${createId("nano")}${extension === "" ? "" : `.${extension}`}`;
};
@@ -12,6 +12,7 @@
"dependencies": {
"@trpc/server": "^10.45.2",
"@webstudio-is/postgrest": "workspace:*",
"@webstudio-is/sdk": "workspace:*",
"@webstudio-is/trpc-interface": "workspace:*",
"zod": "^4.4.3"
},
@@ -1,4 +1,5 @@
import type { Database } from "@webstudio-is/postgrest/index.server";
import { createId } from "@webstudio-is/sdk";
import {
type AppContext,
authorizeProject,
@@ -174,7 +175,7 @@ export const create = async (
},
context: AppContext
) => {
const tokenId = crypto.randomUUID();
const tokenId = createId();
// Only owner of the project can create authorization tokens
const canCreateToken = await authorizeProject.hasProjectPermit(
-1
View File
@@ -61,7 +61,6 @@
"fast-deep-equal": "^3.1.3",
"get-port": "^7.2.0",
"immer": "^10.1.1",
"nanoid": "^5.1.5",
"p-limit": "^6.2.0",
"parse5": "7.3.0",
"path-key": "^4.0.0",
-1
View File
@@ -15,7 +15,6 @@
"@webstudio-is/project-build": "workspace:*",
"@webstudio-is/sdk": "workspace:*",
"@webstudio-is/trpc-interface": "workspace:*",
"nanoid": "^5.1.5",
"type-fest": "^4.37.0",
"zod": "^4.4.3"
},
+3 -2
View File
@@ -5,6 +5,7 @@ import {
createErrorResponse,
getPlanFeaturesByOwnerId,
} from "@webstudio-is/trpc-interface/index.server";
import { createId } from "@webstudio-is/sdk";
import * as projectApi from "@webstudio-is/project/index.server";
import { unpublishBuild } from "@webstudio-is/project-build/server";
import { validateDomain } from "./validate";
@@ -69,7 +70,7 @@ export const create = async (
.from("Domain")
.upsert(
{
id: crypto.randomUUID(),
id: createId(),
domain,
status: "INITIALIZING",
},
@@ -94,7 +95,7 @@ export const create = async (
}
const domainId = domainRow.data.id;
const txtRecord = crypto.randomUUID();
const txtRecord = createId();
const result = await context.postgrest.client.from("ProjectDomain").insert({
domainId,
@@ -1,4 +1,3 @@
import { nanoid } from "nanoid";
import { z } from "zod";
import * as projectApi from "@webstudio-is/project/index.server";
import {
@@ -7,6 +6,7 @@ import {
} from "@webstudio-is/project-build/server";
import { parseDeployment } from "@webstudio-is/project-build/persistence";
import {
createId,
templates as templateSchema,
type Deployment,
} from "@webstudio-is/sdk";
@@ -258,7 +258,7 @@ export const publishStaticProject = async (
{
projectId,
templates,
name = `${projectId}-${nanoid()}.zip`,
name = `${projectId}-${createId("nano")}.zip`,
}: {
projectId: string;
templates: z.infer<typeof templateSchema>[];
@@ -441,4 +441,4 @@ export const verifyProjectDomainResult = async (
return await db.verify(input, context);
};
export const createUnpublishJobId = () => `unpublish-${nanoid()}`;
export const createUnpublishJobId = () => `unpublish-${createId("nano")}`;
-1
View File
@@ -95,7 +95,6 @@
"fast-deep-equal": "^3.1.3",
"fastest-levenshtein": "^1.0.16",
"immer": "^10.1.1",
"nanoid": "^5.1.5",
"papaparse": "^5.5.3",
"slugify": "^1.6.6",
"title-case": "^4.3.2",
+2 -1
View File
@@ -6,6 +6,7 @@ import {
} from "@webstudio-is/trpc-interface/index.server";
import { db as authDb } from "@webstudio-is/authorization-token/index.server";
import {
createId,
dataSource,
type Deployment,
type Resource,
@@ -279,7 +280,7 @@ export const createBuild = async (
const data = createPages();
const pages = removeLegacyProjectSettingsFromPages(data.pages);
const newBuild = await context.postgrest.client.from("Build").insert({
id: crypto.randomUUID(),
id: createId(),
projectId: props.projectId,
pages: JSON.stringify(serializePages(pages)),
projectSettings: JSON.stringify({ meta: {}, compiler: {} }),
@@ -3,7 +3,7 @@ import { builderRuntimeContext } from "./context";
describe("builderRuntimeContext", () => {
test("provides runtime-owned id generation", () => {
expect(builderRuntimeContext.createId()).toEqual(expect.any(String));
expect(builderRuntimeContext.createId()).toMatch(/^[\w-]{21}$/);
expect(builderRuntimeContext.createId()).not.toEqual(
builderRuntimeContext.createId()
);
@@ -1,4 +1,4 @@
import { nanoid } from "nanoid";
import { createId } from "@webstudio-is/sdk";
import type { ContentBlockApplication } from "./content-block-application";
export type BuilderRuntimeContext = {
@@ -11,5 +11,5 @@ export type BuilderRuntimeContext = {
};
export const builderRuntimeContext: BuilderRuntimeContext = {
createId: nanoid,
createId: () => createId("nano"),
};
@@ -1,9 +1,9 @@
// Fragment utilities own extracting, copying, and conflict-checking portable
// Webstudio fragments. Put serialization-style instance subtree cloning and
// fragment asset/style/data remapping here, not live tree placement decisions.
import { nanoid } from "nanoid";
import { migrateCodeTextContentMutable } from "@webstudio-is/project-migrations";
import {
createId,
type Asset,
type Breakpoints,
type DataSource,
@@ -25,6 +25,7 @@ import {
portalComponent,
webstudioFragment,
} from "@webstudio-is/sdk";
import {
findAvailableVariables,
replaceDataSourcesInExpression,
@@ -55,6 +56,8 @@ import {
} from "./style-utils";
import { countDataSourceAssetReferences } from "./assets";
const createNanoId = () => createId("nano");
export type ContentModeCopyableProp = (input: {
prop: Prop;
fragment: WebstudioFragment;
@@ -609,7 +612,7 @@ export const insertWebstudioFragmentCopy = ({
onBreakpointLimitMerge,
metas,
contentModeCopyableProp,
createId = nanoid,
createId = createNanoId,
// In content mode, insertion keeps content-editable instance data, creates
// local styles for inserted instances, and avoids data/resource records.
contentMode = false,
@@ -1,5 +1,4 @@
import { randomUUID } from "node:crypto";
import type { WebstudioFragment } from "@webstudio-is/sdk";
import { createId, type WebstudioFragment } from "@webstudio-is/sdk";
import { renderTemplate } from "@webstudio-is/template";
import { componentMetas } from "@webstudio-is/sdk-components-registry/metas";
import {
@@ -27,7 +26,7 @@ const isTemplateValidationMessage = (message: string) =>
);
const createWebstudioJsxFragmentIdFactory = () => {
const salt = randomUUID();
const salt = createId();
let index = 0;
return () => `__webstudio_jsx_fragment_${salt}_${index++}`;
};
@@ -1,5 +1,6 @@
import { describe, expect, test } from "vitest";
import {
createId,
ROOT_FOLDER_ID,
ROOT_INSTANCE_ID,
encodeDataVariableId,
@@ -48,9 +49,10 @@ import {
Variable,
ws,
} from "@webstudio-is/template";
import { nanoid } from "nanoid";
import { applyBuilderPatchTransactions } from "../state/patch";
const createNanoId = () => createId("nano");
const { deduplicateName, deduplicatePath, joinPath } = pageCopyTesting;
const toMap = <T extends { id: string }>(list: T[]) =>
@@ -78,7 +80,7 @@ test("requires an explicit resolution for conflicting transferred root styles",
}),
},
},
{ createId: nanoid }
{ createId: createNanoId }
)
).toThrow(/root style conflicts require an explicit/i);
});
@@ -94,7 +96,7 @@ test("requires an explicit resolution for conflicting copied root styles", () =>
sourceData: source,
pageId: "source-page",
},
{ createId: nanoid }
{ createId: createNanoId }
)
).toThrow(/root style conflicts require an explicit/i);
});
@@ -114,7 +116,7 @@ test.each([
pageId: "source-page",
rootStyleConflictResolution,
},
{ createId: nanoid }
{ createId: createNanoId }
);
const updated = applyBuilderPatchTransactions(target, [
{ id: "copy-page", payload: mutation.payload },
@@ -498,7 +500,7 @@ describe("insert page copy", () => {
name: "Copy",
path: "/copy",
},
{ createId: nanoid }
{ createId: createNanoId }
);
expect(result.result.pageId).toEqual(expect.any(String));
@@ -572,7 +574,7 @@ describe("insert page copy", () => {
name: "Copy",
path: "/copy",
},
{ createId: nanoid }
{ createId: createNanoId }
);
expect(mutation.payload).toContainEqual({
@@ -728,7 +730,7 @@ describe("insert page copy", () => {
},
},
},
{ createId: nanoid }
{ createId: createNanoId }
)
).toThrow('Copied variable "missing" was not found');
expect(data.pages.pages.size).toBe(2);
@@ -779,7 +781,7 @@ describe("insert page copy", () => {
pageId: "pageId",
path: "/copy",
},
{ createId: nanoid }
{ createId: createNanoId }
)
).toThrow('Page path "/copy" is already in use');
});
@@ -827,7 +829,7 @@ describe("insert page copy", () => {
projectId: "projectId",
folderId: "folderId",
},
{ createId: nanoid }
{ createId: createNanoId }
);
expect(result.result.folderId).toEqual(expect.any(String));
@@ -862,7 +864,7 @@ describe("insert page copy", () => {
projectId: "projectId",
folderId: "missing",
},
{ createId: nanoid }
{ createId: createNanoId }
)
).toThrow("Folder parent folder was not found");
});
@@ -1297,7 +1299,7 @@ describe("insert page copy", () => {
</$.Body>
</ws.root>,
// generate different ids in source and data projects
nanoid
createNanoId
),
};
sourceData.instances.delete(ROOT_INSTANCE_ID);
@@ -1310,7 +1312,7 @@ describe("insert page copy", () => {
<$.Body ws:id="anotherBodyId"></$.Body>
</ws.root>,
// generate different ids in source and data projects
nanoid
createNanoId
),
};
targetData.instances.delete(ROOT_INSTANCE_ID);
@@ -1349,7 +1351,7 @@ describe("insert page copy", () => {
</$.Body>
</ws.root>,
// generate different ids in source and data projects
nanoid
createNanoId
),
};
sourceData.instances.delete(ROOT_INSTANCE_ID);
@@ -1358,7 +1360,7 @@ describe("insert page copy", () => {
rootInstanceId: "anotherBodyId",
}),
// generate different ids in source and data projects
...renderData(<$.Body ws:id="anotherBodyId"></$.Body>, nanoid),
...renderData(<$.Body ws:id="anotherBodyId"></$.Body>, createNanoId),
};
insertPageCopyMutable({
source: { data: sourceData, pageId: sourceData.pages.homePageId },
@@ -1738,7 +1740,7 @@ describe("insert page copy", () => {
const result = duplicatePageTemplate(
data,
{ projectId: "projectId", templateId: "templateId" },
{ createId: nanoid }
{ createId: createNanoId }
);
expect(result.result.templateId).toEqual(expect.any(String));
@@ -1942,7 +1944,7 @@ describe("insert page copy", () => {
name: "Landing",
path: "/landing",
},
{ createId: nanoid }
{ createId: createNanoId }
)
).toThrow('Page path "/landing" is already in use');
});
@@ -6,7 +6,6 @@ import {
resolveFragmentRootStyleConflicts,
} from "./fragment";
import { isFragmentContentModeCopyableProp } from "./content-mode-copy-policy";
import { nanoid } from "nanoid";
import { z } from "zod";
import { parseStringLiteralExpression } from "@webstudio-is/expression";
import {
@@ -18,6 +17,7 @@ import {
unsetExpressionVariables,
} from "./data";
import {
createId,
findParentFolderByChildId,
findPageByIdOrPath,
getFolderById,
@@ -35,6 +35,7 @@ import {
type WebstudioData,
ROOT_INSTANCE_ID,
} from "@webstudio-is/sdk";
import type {
ConflictResolution,
RootStyleConflictResolution,
@@ -65,6 +66,8 @@ import {
type TemplateCopyData,
} from "./data-formats/page-transfer";
const createNanoId = () => createId("nano");
type CreateId = () => string;
const pageDuplicateSubstitutionsInput = z.object({
@@ -285,7 +288,7 @@ const copyPageRootAndBodyMutable = ({
conflictResolution,
rootStyleConflictResolution,
contentModeCopyableProp,
createId = nanoid,
createId = createNanoId,
contentMode = false,
}: {
source: WebstudioData;
@@ -337,7 +340,7 @@ const copyPageFragmentsMutable = ({
rootStyleConflictResolution,
contentModeCopyableProp,
onBreakpointLimitMerge,
createId = nanoid,
createId = createNanoId,
contentMode = false,
}: {
target: WebstudioData;
@@ -553,7 +556,7 @@ export const insertPageFromTemplateMutable = ({
metas,
conflictResolution,
contentModeCopyableProp,
createId = nanoid,
createId = createNanoId,
contentMode = false,
}: {
templateId: PageTemplate["id"];
@@ -615,7 +618,7 @@ const copyPageMutable = ({
projectId,
conflictResolution,
rootStyleConflictResolution,
createId = nanoid,
createId = createNanoId,
}: {
source: { data: WebstudioData; pageId: Page["id"] };
target: { data: WebstudioData; folderId: Folder["id"] };
@@ -770,7 +773,7 @@ export const createPageDuplicatePayload = ({
name,
path,
substitutions,
createId = nanoid,
createId = createNanoId,
}: {
build: Parameters<typeof createWebstudioDataFromBuild>[0]["build"];
assets?: Asset[];
@@ -1336,7 +1339,7 @@ export const insertPageCopyFromFragmentsMutable = ({
rootStyleConflictResolution,
contentModeCopyableProp,
onBreakpointLimitMerge,
createId = nanoid,
createId = createNanoId,
}: {
source: {
page: Page;
@@ -1382,7 +1385,7 @@ export const insertTemplateCopyFromFragmentsMutable = ({
rootStyleConflictResolution,
contentModeCopyableProp,
onBreakpointLimitMerge,
createId = nanoid,
createId = createNanoId,
}: {
source: {
template: PageTemplate;
@@ -1561,7 +1564,7 @@ export const insertFolderCopyFromDataMutable = ({
contentModeCopyableProp,
onBreakpointLimitMerge,
forceFolderCopySuffix = false,
createId = nanoid,
createId = createNanoId,
}: {
source: FolderCopyData;
target: { data: WebstudioData; parentFolderId: Folder["id"] };
@@ -1,6 +1,6 @@
import { nanoid } from "nanoid";
import { toValue } from "@webstudio-is/css-engine";
import {
createId,
getStyleDeclKey,
ROOT_INSTANCE_ID,
type Breakpoint,
@@ -14,6 +14,8 @@ import {
} from "@webstudio-is/sdk";
import { getUniqueNameWithSuffix } from "./style-utils";
const createNanoId = () => createId("nano");
export type ConflictResolution = "ours" | "theirs" | "merge";
export type RootStyleConflictResolution = "ours" | "theirs";
@@ -291,7 +293,7 @@ export const insertStyleSources = ({
breakpoints,
mergedBreakpointIds,
conflictResolution = "theirs",
createId = nanoid,
createId = createNanoId,
}: {
fragmentStyleSources: StyleSource[];
fragmentStyles: StyleDecl[];
@@ -598,7 +600,7 @@ export const insertLocalStyleSourcesWithNewIds = ({
styleSources,
styleSourceSelections,
styles,
createId = nanoid,
createId = createNanoId,
contentMode = false,
breakpoints,
styleSourceIdMap = new Map(),
+7 -4
View File
@@ -1,5 +1,4 @@
import { z } from "zod";
import { nanoid } from "nanoid";
import deepEqual from "fast-deep-equal";
import {
camelCaseProperty,
@@ -15,6 +14,7 @@ import {
type StyleValue,
} from "@webstudio-is/css-engine";
import {
createId,
getStyleDeclKey,
ROOT_INSTANCE_ID,
styleDecl as styleDeclSchema,
@@ -29,6 +29,7 @@ import {
type StyleSources,
type Styles,
} from "@webstudio-is/sdk";
import type { BuilderPatch, BuilderPatchChange } from "../contracts/patch";
import {
paginateOutput,
@@ -46,6 +47,8 @@ import { createPropValue } from "./props";
import { getStyleSourceStylesSignature } from "./style-copy";
import { isBaseWidthBreakpoint } from "./breakpoints";
const createNanoId = () => createId("nano");
export const serializeDesignTokens = ({
styleSources,
styles,
@@ -1706,7 +1709,7 @@ export const createLocalStyleSourcePlan = ({
styleSourceSelection,
styleSources,
instanceId,
createId = nanoid,
createId = createNanoId,
}: {
styleSourceSelection: StyleSourceSelection | undefined;
styleSources: Pick<StyleSources, "get">;
@@ -1762,7 +1765,7 @@ export const createLocalStyleSourcePatchPlan = ({
styleSources,
styleSourceSelection,
shouldCreate,
createId = nanoid,
createId = createNanoId,
}: {
createdLocalSourceIds: Map<Instance["id"], StyleSource["id"]>;
instanceId: Instance["id"];
@@ -1831,7 +1834,7 @@ export const getOrCreateLocalStyleSourceIdMutable = ({
styleSourceSelections,
styleSources,
instanceId,
createId = nanoid,
createId = createNanoId,
}: {
styleSourceSelections: StyleSourceSelections;
styleSources: StyleSources;
@@ -1,5 +1,5 @@
import { nanoid } from "nanoid";
import {
createId,
type Pages,
type Folder,
ROOT_FOLDER_ID,
@@ -19,7 +19,7 @@ export const createRootFolder = (
export const createDefaultPages = ({
rootInstanceId,
systemDataSourceId,
homePageId = nanoid(),
homePageId = createId("nano"),
}: {
rootInstanceId: Instance["id"];
systemDataSourceId?: DataSource["id"];
+7 -7
View File
@@ -1,5 +1,5 @@
import { nanoid } from "nanoid";
import {
createId,
initialBreakpoints,
type Pages,
type WebstudioData,
@@ -11,12 +11,12 @@ import { createRootFolder } from "./shared/pages-utils";
export const createPages = (): WebstudioData => {
const breakpoints = initialBreakpoints.map((breakpoint) => ({
...breakpoint,
id: nanoid(),
id: createId("nano"),
}));
const homePageId = nanoid();
const homeBodyId = nanoid();
const notFoundPageId = nanoid();
const notFoundBodyId = nanoid();
const homePageId = createId("nano");
const homeBodyId = createId("nano");
const notFoundPageId = createId("nano");
const notFoundBodyId = createId("nano");
const data = renderData(
<>
@@ -94,7 +94,7 @@ export const createPages = (): WebstudioData => {
{coreTemplates.builtWithWebstudio.template}
</ws.element>
</>,
nanoid,
() => createId("nano"),
breakpoints
);
+2 -1
View File
@@ -3,6 +3,7 @@ import {
AuthorizationError,
getPlanFeaturesByOwnerId,
} from "@webstudio-is/trpc-interface/index.server";
import { createId } from "@webstudio-is/sdk";
import {
type NotificationType,
type NotificationStatus,
@@ -89,7 +90,7 @@ export const create = async (
}
}
const newId = crypto.randomUUID();
const newId = createId();
const result = await context.postgrest.client
.from("Notification")
+3 -3
View File
@@ -1,4 +1,4 @@
import { nanoid } from "nanoid";
import { createId } from "@webstudio-is/sdk";
import {
authorizeProject,
type AppContext,
@@ -99,7 +99,7 @@ export const create = async (
throw new Error("The user must be authenticated to create a project");
}
const projectId = crypto.randomUUID();
const projectId = createId();
// When creating inside a workspace, the project is owned by the workspace
// owner — not the creating user. This ensures the workspace owner retains
@@ -225,7 +225,7 @@ export const softDeleteProject = async (
.update({
isDeleted: true,
// Free the subdomain — each project needs a unique value
domain: nanoid(),
domain: createId("nano"),
})
.eq("id", projectId);
+2 -1
View File
@@ -1,4 +1,5 @@
import type { Database } from "@webstudio-is/postgrest/index.server";
import { createId } from "@webstudio-is/sdk";
import {
type AppContext,
AuthorizationError,
@@ -67,7 +68,7 @@ export const create = async (
const result = await context.postgrest.client
.from("Workspace")
.insert({
id: crypto.randomUUID(),
id: createId(),
name: trimmed,
isDefault: false,
userId,
+1 -2
View File
@@ -37,8 +37,7 @@
"@webstudio-is/icons": "workspace:^",
"@webstudio-is/image": "workspace:*",
"@webstudio-is/sdk": "workspace:*",
"change-case": "^5.4.4",
"nanoid": "^5.1.5"
"change-case": "^5.4.4"
},
"exports": {
".": {
-1
View File
@@ -31,7 +31,6 @@
"@emotion/hash": "^0.9.2",
"@webstudio-is/multiplayer-protocol": "workspace:*",
"nanostores": "^0.11.3",
"nanoid": "^5.1.5",
"immerhin": "*",
"partysocket": "^1.1.18"
},
+3 -18
View File
@@ -1192,9 +1192,6 @@ importers:
immer:
specifier: ^10.1.1
version: 10.1.1
nanoid:
specifier: ^5.1.5
version: 5.1.5
warn-once:
specifier: ^0.1.1
version: 0.1.1
@@ -1226,6 +1223,9 @@ importers:
'@webstudio-is/postgrest':
specifier: workspace:*
version: link:../postgrest
'@webstudio-is/sdk':
specifier: workspace:*
version: link:../sdk
'@webstudio-is/trpc-interface':
specifier: workspace:*
version: link:../trpc-interface
@@ -1311,9 +1311,6 @@ importers:
immer:
specifier: ^10.1.1
version: 10.1.1
nanoid:
specifier: ^5.1.5
version: 5.1.5
p-limit:
specifier: ^6.2.0
version: 6.2.0
@@ -1911,9 +1908,6 @@ importers:
'@webstudio-is/trpc-interface':
specifier: workspace:*
version: link:../trpc-interface
nanoid:
specifier: ^5.1.5
version: 5.1.5
type-fest:
specifier: ^4.37.0
version: 4.37.0
@@ -2332,9 +2326,6 @@ importers:
immer:
specifier: ^10.1.1
version: 10.1.1
nanoid:
specifier: ^5.1.5
version: 5.1.5
papaparse:
specifier: ^5.5.3
version: 5.5.3
@@ -2514,9 +2505,6 @@ importers:
change-case:
specifier: ^5.4.4
version: 5.4.4
nanoid:
specifier: ^5.1.5
version: 5.1.5
devDependencies:
'@types/react':
specifier: ^18.2.70
@@ -3057,9 +3045,6 @@ importers:
immerhin:
specifier: '*'
version: 0.10.0
nanoid:
specifier: ^5.1.5
version: 5.1.5
nanostores:
specifier: ^0.11.3
version: 0.11.3