Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
491 changes: 491 additions & 0 deletions .cursor/rules/ts-code-migration.mdc

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"knip": "knip",
"lint": "turbo run lint --continue",
"lint:affected": "turbo run lint --affected --continue",
"lint:only": "pnpm run -r lint",
"meta:test": "meta-updater --test",
"meta:update": "meta-updater",
"precheck": "turbo run lint prettier:check typecheck --output-logs=errors-only --affected --continue && pnpm run knip",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,16 @@ export function generateConfig({
// we should prefer logger over console
'no-console': 'error',
// // ensure we alphabetize imports for easier reading
// 'import/order': [
// 'error',
// {
// pathGroups: [
// { pattern: 'src/**', group: 'external', position: 'after' },
// { pattern: '@src/**', group: 'external', position: 'after' },
// ],
// alphabetize: { order: 'asc', caseInsensitive: true },
// },
// ],
'import/order': [
'error',
{
pathGroups: [
{ pattern: 'src/**', group: 'external', position: 'after' },
{ pattern: '@src/**', group: 'external', position: 'after' },
],
alphabetize: { order: 'asc', caseInsensitive: true },
},
],
// ensure we don't have devDependencies imported in production code
'import/no-extraneous-dependencies': [
'error',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,19 @@ export const typescriptGenerator = createGenerator({
});
}

const sharedRenderOptions = {
importSortOptions: {
groups: [
'builtin',
'external',
'internal',
['parent', 'sibling'],
'index',
] as const,
internalPatterns,
},
};

function renderTemplateFile(
payload: RenderTsTemplateFileActionInput,
): BuilderAction {
Expand All @@ -352,9 +365,7 @@ export const typescriptGenerator = createGenerator({
resolveModule(moduleSpecifier) {
return resolveModuleSpecifier(moduleSpecifier, directory);
},
importSortOptions: {
internalPatterns,
},
...sharedRenderOptions,
},
});
}
Expand All @@ -379,9 +390,7 @@ export const typescriptGenerator = createGenerator({
sourceDirectory,
);
},
importSortOptions: {
internalPatterns,
},
...sharedRenderOptions,
},
}),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface SortImportDeclarationsOptions {
* Groups to sort imports by. Each inner array represents imports
* that should be grouped together.
*/
groups: (ImportSortGroup | ImportSortGroup[])[];
groups: readonly (ImportSortGroup | readonly ImportSortGroup[])[];
/**
* Whether to ignore case when sorting imports
*
Expand Down
36 changes: 24 additions & 12 deletions packages/core-generators/src/renderers/typescript/ts-code-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,27 @@ export const TsCodeUtils = {
};
},

/**
* Merge an array of code fragments into an array literal.
*
* @param fragments - The code fragments to merge.
* @returns The merged code fragment as an array literal.
*/
mergeFragmentsAsArrayPresorted(
fragments: (string | TsCodeFragment)[],
): TsCodeFragment {
return {
contents: `[${fragments
.map((fragment) =>
typeof fragment === 'string' ? fragment : fragment.contents,
)
.join(',\n')}]`,
...mergeFragmentImportsAndHoistedFragments(
fragments.filter(isTsCodeFragment),
),
};
},

/**
* Merge a map of code fragments into an array literal. The fragments are sorted by key
* to ensure deterministic output.
Expand All @@ -277,18 +298,9 @@ export const TsCodeUtils = {
const map =
objOrMap instanceof Map ? objOrMap : new Map(Object.entries(objOrMap));
const sortedFragmentEntries = sortBy([...map.entries()], [([key]) => key]);
return {
contents: `[${sortedFragmentEntries
.map(([, fragment]) =>
typeof fragment === 'string' ? fragment : fragment.contents,
)
.join(',\n')}]`,
...mergeFragmentImportsAndHoistedFragments(
sortedFragmentEntries
.map(([, fragment]) => fragment)
.filter(isTsCodeFragment),
),
};
return this.mergeFragmentsAsArrayPresorted(
sortedFragmentEntries.map(([, fragment]) => fragment),
);
},

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/project-builder-lib/src/plugins/imports/loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toposort } from '@halfdomelabs/utils';
import { toposortOrdered } from '@halfdomelabs/utils';
import { keyBy, mapValues } from 'es-toolkit';

import { stripUndefinedValues } from '@src/utils/strip.js';
Expand Down Expand Up @@ -85,7 +85,7 @@ export function getOrderedPluginModuleInitializationSteps(

const nodes = pluginModules.map((p) => p.id);

return toposort(nodes, edges);
return toposortOrdered(nodes, edges);
}

export function initializeOrderedPluginModules(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function buildAdmin(builder: AdminAppEntryBuilder): GeneratorBundle {
})
: undefined,
routes: [
compileAuthPages(builder, appConfig.allowedRoles),
compileAuthPages(builder),
...compileAdminFeatures(builder),
],
},
Expand Down
54 changes: 11 additions & 43 deletions packages/project-builder-server/src/compiler/lib/web-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,7 @@ import {
auth0CallbackGenerator,
auth0ComponentsGenerator,
auth0HooksGenerator,
authApolloGenerator,
authComponentsGenerator,
authHooksGenerator,
authIdentifyGenerator,
authLayoutGenerator,
authLoginPageGenerator,
authPagesGenerator,
authServiceGenerator,
reactAuth0Generator,
reactRoutesGenerator,
} from '@halfdomelabs/react-generators';
Expand All @@ -29,56 +22,31 @@ export function compileAuthFeatures(
if (builder.appConfig.type === 'web' && !builder.appConfig.includeAuth) {
return undefined;
}
if (builder.projectDefinition.auth?.useAuth0) {
return {
auth: reactAuth0Generator({
callbackPath: 'auth/auth0-callback',
}),
authHooks: auth0HooksGenerator({}),
authIdentify: authIdentifyGenerator({}),
auth0Apollo: auth0ApolloGenerator({}),
auth0Components: auth0ComponentsGenerator({}),
};
if (!builder.projectDefinition.auth?.useAuth0) {
throw new Error('Auth0 is not enabled');
Comment on lines +25 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Throwing on missing Auth0 may be too rigid—consider a graceful fallback

Both compileAuthFeatures and compileAuthPages now throw when useAuth0 is falsy.
In projects where includeAuth is true but the provider is intentionally left undecided, this hard-failure prevents the rest of the compilation pipeline from running (e.g. for mobile targets or future providers). Returning undefined (like the early-exit above) or a more descriptive, typed GeneratorDisabledError would allow callers to ignore unsupported auth providers without crashing the build.

Also applies to: 42-44


🏁 Script executed:

#!/bin/bash
# Inspect compileAuthFeatures implementation
rg -n "compileAuthFeatures" -A5 packages/project-builder-server/src/compiler/lib/web-auth.ts

# Inspect compileAuthPages implementation
rg -n "compileAuthPages" -A5 packages/project-builder-server/src/compiler/lib/web-auth.ts

# Check if a GeneratorDisabledError type already exists
rg -n "GeneratorDisabledError"

Length of output: 662


🏁 Script executed:

#!/bin/bash
# Search for all occurrences of useAuth0 in the file to see where compileAuthFeatures references it
rg -n "useAuth0" packages/project-builder-server/src/compiler/lib/web-auth.ts

Length of output: 185


Gracefully disable Auth0 instead of hard‐failing

Both compileAuthFeatures (at lines 25–26) and compileAuthPages (at lines 42–44) currently do:

if (!builder.projectDefinition.auth?.useAuth0) {
  throw new Error('Auth0 is not enabled');
}

Throwing a generic Error here will abort your entire build whenever useAuth0 is false. To allow projects that includeAuth but haven’t picked a provider (or target platforms that don’t support Auth0) to continue:

• Change each function to return undefined when Auth0 is disabled (and update its signature to allow undefined)
• —or—
• Define and throw a custom GeneratorDisabledError so callers can catch and skip disabled generators

For example, in packages/project-builder-server/src/compiler/lib/web-auth.ts:

-export function compileAuthPages(...): GeneratorBundle {
-  if (!builder.projectDefinition.auth?.useAuth0) {
-    throw new Error('Auth0 is not enabled');
-  }
+export function compileAuthPages(...): GeneratorBundle | undefined {
+  if (!builder.projectDefinition.auth?.useAuth0) {
+    // Auth0 is disabled—skip this generator
+    return undefined;
+  }

And similarly update compileAuthFeatures. Ensure downstream callers check for undefined (or catch your new GeneratorDisabledError) so the rest of the compilation pipeline can proceed.

}
return {
authService: authServiceGenerator({}),
authHooks: authHooksGenerator({}),
authIdentify: authIdentifyGenerator({}),
authApollo: authApolloGenerator({}),
authComponents: authComponentsGenerator({
loginPath: '/auth/login',
auth: reactAuth0Generator({
callbackPath: 'auth/auth0-callback',
}),
authHooks: auth0HooksGenerator({}),
authIdentify: authIdentifyGenerator({}),
auth0Apollo: auth0ApolloGenerator({}),
auth0Components: auth0ComponentsGenerator({}),
};
}

export function compileAuthPages(
builder: AppEntryBuilder<AppConfig>,
allowedRoles: string[] = [],
): GeneratorBundle {
if (builder.projectDefinition.auth?.useAuth0) {
return reactRoutesGenerator({
id: 'auth',
name: 'auth',
children: {
auth: auth0CallbackGenerator({}),
},
});
if (!builder.projectDefinition.auth?.useAuth0) {
throw new Error('Auth0 is not enabled');
}

return reactRoutesGenerator({
id: 'auth',
name: 'auth',
children: {
auth: authPagesGenerator({
children: {
layout: authLayoutGenerator({
name: 'AuthLayout',
}),
login: authLoginPageGenerator({
allowedRoles: allowedRoles.map((r) => builder.nameFromId(r)),
}),
},
}),
auth: auth0CallbackGenerator({}),
},
});
}
2 changes: 1 addition & 1 deletion packages/project-builder-server/src/compiler/web/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function buildReact(builder: AppEntryBuilder<WebAppConfig>): GeneratorBundle {
children: {
reactNotFoundHandler: reactNotFoundHandlerGenerator({}),
auth: builder.appConfig.includeAuth
? compileAuthPages(builder, appConfig.allowedRoles)
? compileAuthPages(builder)
: undefined,
},
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Console } from '@src/components';
import { useProjects } from '@src/hooks/useProjects';
import { useSyncMetadata } from '@src/hooks/useSyncMetadata';
import { cancelSync, startSync } from '@src/services/api';
import { formatError } from '@src/services/error-formatter';
import { formatError, logAndFormatError } from '@src/services/error-formatter';

import { PackageSyncStatus } from './PackageSyncStatus';

Expand Down Expand Up @@ -48,7 +48,7 @@ function ProjectSyncModal({ className }: Props): React.JSX.Element {
}

startSync(currentProjectId).catch((error: unknown) =>
toast.error(formatError(error)),
toast.error(logAndFormatError(error)),
);
setIsOpen(true);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ImportMapper } from '@halfdomelabs/core-generators';

import {
projectScope,
tsCodeFragment,
Expand All @@ -7,13 +9,17 @@ import {
import {
createGenerator,
createGeneratorTask,
createProviderTask,
createProviderType,
} from '@halfdomelabs/sync';
import { z } from 'zod';

import { reactApolloProvider } from '@src/generators/apollo/react-apollo/react-apollo.generator.js';
import { reactComponentsProvider } from '@src/generators/core/react-components/react-components.generator.js';
import { reactConfigProvider } from '@src/generators/core/react-config/react-config.generator.js';
import {
reactConfigImportsProvider,
reactConfigProvider,
} from '@src/generators/core/react-config/react-config.generator.js';
import { reactErrorProvider } from '@src/generators/core/react-error/react-error.generator.js';
import { reactRoutesProvider } from '@src/providers/routes.js';

Expand All @@ -31,11 +37,18 @@ export const adminBullBoardGenerator = createGenerator({
generatorFileUrl: import.meta.url,
descriptorSchema,
buildTasks: ({ bullBoardUrl }) => ({
reactConfig: createProviderTask(reactConfigProvider, (reactConfig) => {
reactConfig.configEntries.set('VITE_BULL_BOARD_BASE', {
comment: 'Base path for bull-board site',
validator: 'z.string().min(1)',
devDefaultValue: bullBoardUrl,
});
}),
main: createGeneratorTask({
dependencies: {
typescript: typescriptProvider,
reactComponents: reactComponentsProvider,
reactConfig: reactConfigProvider,
reactConfigImports: reactConfigImportsProvider,
reactError: reactErrorProvider,
reactApollo: reactApolloProvider,
reactRoutes: reactRoutesProvider,
Expand All @@ -46,7 +59,7 @@ export const adminBullBoardGenerator = createGenerator({
run({
typescript,
reactComponents,
reactConfig,
reactConfigImports,
reactError,
reactApollo,
reactRoutes,
Expand All @@ -58,9 +71,16 @@ export const adminBullBoardGenerator = createGenerator({
adminBullBoard: {},
},
build: async (builder) => {
const importMappers = [
const importMappers: ImportMapper[] = [
reactComponents,
reactConfig,
{
getImportMap: () => ({
'%react-config': {
path: reactConfigImports.config.source,
allowedImports: ['config'],
},
}),
},
reactError,
reactApollo,
];
Expand All @@ -77,12 +97,6 @@ export const adminBullBoardGenerator = createGenerator({
),
});

reactConfig.configEntries.set('VITE_BULL_BOARD_BASE', {
comment: 'Base path for bull-board site',
validator: 'z.string().min(1)',
devDefaultValue: bullBoardUrl,
});

await builder.apply(
typescript.createCopyFilesAction({
paths: ['index.tsx', 'bull-board.gql'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@halfdomelabs/sync';
import { z } from 'zod';

import { authHooksProvider } from '@src/generators/auth/auth-hooks/auth-hooks.generator.js';
import { authHooksProvider } from '@src/generators/auth/_providers/auth-hooks.js';
import { reactComponentsProvider } from '@src/generators/core/react-components/react-components.generator.js';
import { reactRoutesProvider } from '@src/providers/routes.js';
import { createRouteElement } from '@src/utils/routes.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
import { quot } from '@halfdomelabs/utils';
import { z } from 'zod';

import { authComponentsProvider } from '@src/generators/auth/auth-components/auth-components.generator.js';
import { authHooksProvider } from '@src/generators/auth/auth-hooks/auth-hooks.generator.js';
import { authComponentsProvider } from '@src/generators/auth/_providers/auth-components.js';
import { authHooksProvider } from '@src/generators/auth/_providers/auth-hooks.js';
import { reactComponentsProvider } from '@src/generators/core/react-components/react-components.generator.js';
import { reactTailwindProvider } from '@src/generators/core/react-tailwind/react-tailwind.generator.js';
import { reactRoutesProvider } from '@src/providers/routes.js';
Expand Down
Loading