lowdefy 5.6.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/commands/agentSetup/agentSetup.js +85 -0
  2. package/dist/commands/agentSetup/agentsMd.js +59 -0
  3. package/dist/commands/agentSetup/buildMcpServerEntry.js +21 -0
  4. package/dist/commands/agentSetup/findProjectRoot.js +39 -0
  5. package/dist/commands/agentSetup/getDevCommand.js +50 -0
  6. package/dist/commands/agentSetup/skillMd.js +48 -0
  7. package/dist/commands/agentSetup/upsertAgentsMdSection.js +63 -0
  8. package/dist/commands/agentSetup/upsertClaudeSettings.js +56 -0
  9. package/dist/commands/agentSetup/upsertMcpServer.js +83 -0
  10. package/dist/commands/agentSetup/writeSkillFile.js +32 -0
  11. package/dist/commands/build/build.js +3 -3
  12. package/dist/commands/dev/dev.js +13 -8
  13. package/dist/commands/dev/resolveMockUser.js +37 -0
  14. package/dist/commands/dev/runDevServer.js +17 -11
  15. package/dist/commands/dockerOutput/dockerOutput.js +99 -0
  16. package/dist/commands/emails/emails.js +84 -0
  17. package/dist/commands/emails/generateEmailShims.js +94 -0
  18. package/dist/commands/emails/installReactEmail.js +53 -0
  19. package/dist/commands/emails/readNotificationArtifacts.js +37 -0
  20. package/dist/commands/emails/runEmailPreview.js +47 -0
  21. package/dist/commands/emails/warnMissingDataKeys.js +45 -0
  22. package/dist/commands/init-docker/Dockerfile +37 -19
  23. package/dist/commands/init-docker/dockerignore +8 -0
  24. package/dist/commands/init-docker/initDocker.js +3 -1
  25. package/dist/commands/init-vercel/README.md +94 -20
  26. package/dist/commands/init-vercel/initVercel.js +12 -2
  27. package/dist/commands/init-vercel/vercel.build.sh +8 -0
  28. package/dist/commands/init-vercel/vercel.install.sh +4 -2
  29. package/dist/commands/init-vercel/vercel.json +5 -0
  30. package/dist/commands/start/runStart.js +1 -2
  31. package/dist/commands/vercelOutput/apiHandler.js +83 -0
  32. package/dist/commands/vercelOutput/vercelOutput.js +138 -0
  33. package/dist/program.js +24 -2
  34. package/dist/utils/checkPortAvailable.js +9 -16
  35. package/dist/utils/copyTracedFiles.js +94 -0
  36. package/dist/utils/findTraceBase.js +45 -0
  37. package/dist/utils/getDirectories.js +2 -1
  38. package/dist/utils/getOptions.js +2 -0
  39. package/dist/utils/installServer.js +31 -23
  40. package/dist/utils/{runNextBuild.js → runClientBuild.js} +11 -14
  41. package/package.json +14 -13
@@ -14,7 +14,23 @@
14
14
  limitations under the License.
15
15
  */ import { spawnProcess } from '@lowdefy/node-utils';
16
16
  import { createStdOutLineHandler } from '@lowdefy/logger/cli';
17
+ import resolveMockUser from './resolveMockUser.js';
17
18
  async function runDevServer({ context, directory }) {
19
+ const env = {
20
+ ...process.env,
21
+ LOWDEFY_BUILD_REF_RESOLVER: context.options.refResolver,
22
+ LOWDEFY_DIRECTORY_CONFIG: context.directories.config,
23
+ LOWDEFY_LOG_LEVEL: context.options.logLevel,
24
+ LOWDEFY_SERVER_DEV_OPEN_BROWSER: !!context.options.open,
25
+ LOWDEFY_SERVER_DEV_WATCH: JSON.stringify(context.options.watch),
26
+ LOWDEFY_SERVER_DEV_WATCH_IGNORE: JSON.stringify(context.options.watchIgnore),
27
+ PORT: context.options.port
28
+ };
29
+ // Set only when requested — an undefined value would clobber the LOWDEFY_DEV_USER
30
+ // inherited from process.env above.
31
+ if (context.options.mockUser) {
32
+ env.LOWDEFY_DEV_USER = resolveMockUser(context.options.mockUser);
33
+ }
18
34
  await spawnProcess({
19
35
  args: [
20
36
  'run',
@@ -28,17 +44,7 @@ async function runDevServer({ context, directory }) {
28
44
  cwd: directory,
29
45
  // https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2#command-injection-via-args-parameter-of-child_processspawn-without-shell-option-enabled-on-windows-cve-2024-27980---high
30
46
  shell: process.platform === 'win32',
31
- env: {
32
- ...process.env,
33
- LOWDEFY_BUILD_REF_RESOLVER: context.options.refResolver,
34
- LOWDEFY_DIRECTORY_CONFIG: context.directories.config,
35
- LOWDEFY_LOG_LEVEL: context.options.logLevel,
36
- LOWDEFY_SERVER_DEV_OPEN_BROWSER: !!context.options.open,
37
- LOWDEFY_SERVER_DEV_WATCH: JSON.stringify(context.options.watch),
38
- LOWDEFY_SERVER_DEV_WATCH_IGNORE: JSON.stringify(context.options.watchIgnore),
39
- PORT: context.options.port,
40
- NEXT_TELEMETRY_DISABLED: context.options.disableTelemetry ? '1' : undefined
41
- }
47
+ env
42
48
  },
43
49
  silent: false
44
50
  });
@@ -0,0 +1,99 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import fs from 'fs';
16
+ import path from 'path';
17
+ import { cleanDirectory, copyFileOrDirectory, writeFile } from '@lowdefy/node-utils';
18
+ import copyTracedFiles from '../../utils/copyTracedFiles.js';
19
+ import findTraceBase from '../../utils/findTraceBase.js';
20
+ // The boot script chdirs into the server directory inside the output (it is nested when the app
21
+ // is a pnpm workspace member) so the server's cwd-relative reads of build/ and dist/client work,
22
+ // then imports the entrypoint. .mjs so it runs as ESM regardless of a package.json at its root.
23
+ function startScript({ relServer }) {
24
+ const relServerPosix = relServer === '' ? '.' : relServer.split(path.sep).join('/');
25
+ return `// Generated by \`lowdefy docker-output\` — boots the server from the traced output.
26
+ import path from 'node:path';
27
+ import url from 'node:url';
28
+
29
+ const serverDirectory = path.join(
30
+ path.dirname(url.fileURLToPath(import.meta.url)),
31
+ '${relServerPosix}'
32
+ );
33
+ process.chdir(serverDirectory);
34
+ await import(url.pathToFileURL(path.join(serverDirectory, 'src', 'index.js')));
35
+ `;
36
+ }
37
+ // Assembles a minimal production runtime at .lowdefy/docker from an already-built server directory.
38
+ // Instead of shipping the full node_modules (which includes the client-side block packages that are
39
+ // already compiled into dist/client), the runtime file closure of the Hono server is traced with
40
+ // @vercel/nft — the same mechanism the Vercel deployment uses. Run after `lowdefy build`.
41
+ async function dockerOutput({ context }) {
42
+ const { directories, logger } = context;
43
+ logger.info('Assembling Docker runtime output.');
44
+ const serverDirectory = path.resolve(directories.server);
45
+ const buildDirectory = path.resolve(directories.build);
46
+ const clientDirectory = path.join(serverDirectory, 'dist', 'client');
47
+ const outputDirectory = path.join(path.resolve(directories.config), '.lowdefy', 'docker');
48
+ const entrypoints = [
49
+ path.join(serverDirectory, 'src', 'index.js'),
50
+ path.join(serverDirectory, 'src', 'app.js')
51
+ ];
52
+ entrypoints.forEach((entrypoint)=>{
53
+ if (!fs.existsSync(entrypoint)) {
54
+ throw new Error(`Cannot trace "${entrypoint}" — run "lowdefy build" first.`);
55
+ }
56
+ });
57
+ if (!fs.existsSync(clientDirectory)) {
58
+ throw new Error(`Cannot find built client at "${clientDirectory}" — run "lowdefy build" first.`);
59
+ }
60
+ // Paths are preserved relative to the trace base (the pnpm workspace root when the server
61
+ // directory is a workspace member), so the server's relative node_modules symlinks still
62
+ // resolve inside the output. The server lands at <relServer>/ inside the output directory.
63
+ const base = findTraceBase({
64
+ serverDirectory
65
+ });
66
+ const relServer = path.relative(base, serverDirectory);
67
+ const outputServerDirectory = path.join(outputDirectory, relServer);
68
+ // Start clean so artifacts from a previous build do not leak into the image.
69
+ await cleanDirectory(outputDirectory);
70
+ // 1. The runtime file closure of the Hono server, traced with @vercel/nft — the app source, the
71
+ // build-generated plugin import files, and exactly the node_modules files they resolve to
72
+ // (through pnpm's symlinks, which are preserved).
73
+ const fileCount = await copyTracedFiles({
74
+ base,
75
+ entrypoints,
76
+ functionDirectory: outputDirectory,
77
+ logger,
78
+ processCwd: serverDirectory
79
+ });
80
+ logger.info(`Traced ${fileCount} runtime files into the output.`);
81
+ // 2. Artifacts the server reads from the filesystem at runtime (not imported, so not traced),
82
+ // plus src/ and lib/ wholesale as belt-and-braces alongside the traced copies. Unlike the
83
+ // Vercel output, dist/client ships in full — the Node server serves the static assets itself.
84
+ await copyFileOrDirectory(path.join(serverDirectory, 'src'), path.join(outputServerDirectory, 'src'));
85
+ await copyFileOrDirectory(path.join(serverDirectory, 'lib'), path.join(outputServerDirectory, 'lib'));
86
+ await copyFileOrDirectory(buildDirectory, path.join(outputServerDirectory, 'build'));
87
+ await copyFileOrDirectory(clientDirectory, path.join(outputServerDirectory, 'dist', 'client'));
88
+ await copyFileOrDirectory(path.join(serverDirectory, 'package.json'), path.join(outputServerDirectory, 'package.json'));
89
+ // 3. A uniform boot script so the container CMD is the same whether or not the server is nested
90
+ // under a workspace path inside the output.
91
+ await writeFile(path.join(outputDirectory, 'start.mjs'), startScript({
92
+ relServer
93
+ }));
94
+ logger.info({
95
+ spin: 'succeed'
96
+ }, 'Docker runtime output assembled at .lowdefy/docker.');
97
+ await context.sendTelemetry();
98
+ }
99
+ export default dockerOutput;
@@ -0,0 +1,84 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import addCustomPluginsAsDeps from '../../utils/addCustomPluginsAsDeps.js';
16
+ import getServer from '../../utils/getServer.js';
17
+ import installServer from '../../utils/installServer.js';
18
+ import resetServerPackageJson from '../../utils/resetServerPackageJson.js';
19
+ import runLowdefyBuild from '../../utils/runLowdefyBuild.js';
20
+ import generateEmailShims from './generateEmailShims.js';
21
+ import installReactEmail from './installReactEmail.js';
22
+ import readNotificationArtifacts from './readNotificationArtifacts.js';
23
+ import runEmailPreview from './runEmailPreview.js';
24
+ import warnMissingDataKeys from './warnMissingDataKeys.js';
25
+ async function emails({ context }) {
26
+ context.logger.info('Building app for email preview.');
27
+ const directory = context.directories.server;
28
+ // The lowdefy build pipeline minus the client build — guarantees
29
+ // build/notifications/*.json exists and the second install has installed
30
+ // react-email, @lowdefy/email-templates and any custom template plugins.
31
+ await getServer({
32
+ context,
33
+ packageName: '@lowdefy/server',
34
+ directory
35
+ });
36
+ await resetServerPackageJson({
37
+ context,
38
+ directory
39
+ });
40
+ await addCustomPluginsAsDeps({
41
+ context,
42
+ directory
43
+ });
44
+ await installServer({
45
+ context,
46
+ directory
47
+ });
48
+ await runLowdefyBuild({
49
+ context,
50
+ directory
51
+ });
52
+ await installServer({
53
+ context,
54
+ directory
55
+ });
56
+ const { notifications, appEmail, notificationTypes } = await readNotificationArtifacts({
57
+ context
58
+ });
59
+ if (notifications.length === 0) {
60
+ context.logger.warn('No notifications configured. Add a "notifications:" section to preview emails.');
61
+ return;
62
+ }
63
+ await installReactEmail({
64
+ context,
65
+ directory
66
+ });
67
+ await generateEmailShims({
68
+ context,
69
+ notifications,
70
+ appEmail,
71
+ notificationTypes
72
+ });
73
+ await warnMissingDataKeys({
74
+ context,
75
+ notifications,
76
+ notificationTypes
77
+ });
78
+ context.logger.info('Preview shims generated. Config changes (properties, testData) need a rerun of "lowdefy emails"; template code changes hot-reload.');
79
+ await context.sendTelemetry();
80
+ await runEmailPreview({
81
+ context
82
+ });
83
+ }
84
+ export default emails;
@@ -0,0 +1,94 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import fs from 'fs';
16
+ import path from 'path';
17
+ import { type } from '@lowdefy/helpers';
18
+ function pascalCase(id) {
19
+ return id.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part)=>part.charAt(0).toUpperCase() + part.slice(1)).join('');
20
+ }
21
+ function createShimContent({ notification, typeDefinition, appEmail }) {
22
+ const componentName = `${pascalCase(notification.notificationId)}Preview`;
23
+ // Shallow merge, matching the runtime theme resolution.
24
+ const theme = {
25
+ ...appEmail,
26
+ ...notification.theme ?? {}
27
+ };
28
+ // The preview has no serverUrl — match the runtime fallback for relative
29
+ // logos (drop it, EmailLayout shows the companyName text header) instead of
30
+ // rendering a broken image.
31
+ if (type.isString(theme.logo) && theme.logo.startsWith('/') && !theme.logo.startsWith('//')) {
32
+ delete theme.logo;
33
+ }
34
+ const config = {
35
+ id: notification.notificationId,
36
+ type: notification.type,
37
+ properties: notification.properties ?? {},
38
+ theme,
39
+ testData: notification.testData ?? {}
40
+ };
41
+ // React.createElement instead of JSX syntax so the shim is immune to the
42
+ // preview toolchain's JSX runtime configuration.
43
+ return `// Generated by lowdefy emails. Do not edit.
44
+ import React from 'react';
45
+ import { ${typeDefinition.originalTypeName} as Template } from '${typeDefinition.package}/notifications';
46
+ import { buildPreviewProps } from '@lowdefy/email-templates';
47
+
48
+ const config = ${JSON.stringify(config, null, 2)};
49
+
50
+ function ${componentName}(props) {
51
+ return React.createElement(Template, props);
52
+ }
53
+ // A wrapper component per shim — setting PreviewProps on the shared imported
54
+ // template would clobber across notifications using the same template type.
55
+ ${componentName}.PreviewProps = buildPreviewProps({ Template, config });
56
+
57
+ export default ${componentName};
58
+ `;
59
+ }
60
+ async function generateEmailShims({ context, notifications, appEmail, notificationTypes }) {
61
+ const emailsDirectory = context.directories.emails;
62
+ const serverDirectory = context.directories.server;
63
+ await fs.promises.rm(emailsDirectory, {
64
+ recursive: true,
65
+ force: true
66
+ });
67
+ await fs.promises.mkdir(emailsDirectory, {
68
+ recursive: true
69
+ });
70
+ // Bare imports in the shims resolve against the server's installed tree.
71
+ // 'junction' works unprivileged on Windows; on POSIX it creates a normal
72
+ // directory symlink.
73
+ await fs.promises.symlink(path.join(serverDirectory, 'node_modules'), path.join(emailsDirectory, 'node_modules'), 'junction');
74
+ for (const notification of notifications){
75
+ const typeDefinition = notificationTypes[notification.type];
76
+ if (!typeDefinition) {
77
+ context.logger.warn(`Notification "${notification.notificationId}": template type "${notification.type}" not found in the build types map. Skipping preview.`);
78
+ continue;
79
+ }
80
+ const content = createShimContent({
81
+ notification,
82
+ typeDefinition,
83
+ appEmail
84
+ });
85
+ // Module notifications have scoped ids ("invites/invite-user") — nested
86
+ // shim paths group the preview sidebar by module entry.
87
+ const shimPath = path.join(emailsDirectory, `${notification.notificationId}.jsx`);
88
+ await fs.promises.mkdir(path.dirname(shimPath), {
89
+ recursive: true
90
+ });
91
+ await fs.promises.writeFile(shimPath, content);
92
+ }
93
+ }
94
+ export default generateEmailShims;
@@ -0,0 +1,53 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import fs from 'fs';
16
+ import path from 'path';
17
+ import { spawnProcess } from '@lowdefy/node-utils';
18
+ const reactEmailVersion = '6.6.6';
19
+ // The react-email preview CLI is a dev-only tool — the build never adds it to
20
+ // the server package, so production installs don't carry it. Install it
21
+ // just-in-time into the server directory when the emails command runs.
22
+ async function installReactEmail({ context, directory }) {
23
+ const emailBin = path.join(directory, 'node_modules', '.bin', process.platform === 'win32' ? 'email.cmd' : 'email');
24
+ if (fs.existsSync(emailBin)) {
25
+ return;
26
+ }
27
+ context.logger.info({
28
+ spin: 'start'
29
+ }, 'Installing the react-email preview server.');
30
+ try {
31
+ await spawnProcess({
32
+ command: context.pnpmCmd,
33
+ args: [
34
+ 'add',
35
+ '--save-dev',
36
+ `react-email@${reactEmailVersion}`
37
+ ],
38
+ stdOutLineHandler: (line)=>context.logger.debug(line),
39
+ processOptions: {
40
+ cwd: directory,
41
+ // https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2#command-injection-via-args-parameter-of-child_processspawn-without-shell-option-enabled-on-windows-cve-2024-27980---high
42
+ shell: process.platform === 'win32'
43
+ }
44
+ });
45
+ } catch (error) {
46
+ context.logger.info({
47
+ spin: 'fail'
48
+ }, 'Installing the react-email preview server.');
49
+ throw new Error('react-email installation failed.');
50
+ }
51
+ context.logger.info('react-email preview server installed.');
52
+ }
53
+ export default installReactEmail;
@@ -0,0 +1,37 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import fs from 'fs';
16
+ import path from 'path';
17
+ async function readNotificationArtifacts({ context }) {
18
+ const buildDirectory = context.directories.build;
19
+ const notificationsDirectory = path.join(buildDirectory, 'notifications');
20
+ let notifications = [];
21
+ if (fs.existsSync(notificationsDirectory)) {
22
+ // Recursive — module notifications write scoped artifacts in
23
+ // subdirectories (notifications/<entry>/<id>.json).
24
+ const files = await fs.promises.readdir(notificationsDirectory, {
25
+ recursive: true
26
+ });
27
+ notifications = await Promise.all(files.filter((file)=>file.endsWith('.json')).map(async (file)=>JSON.parse(await fs.promises.readFile(path.join(notificationsDirectory, file), 'utf8'))));
28
+ }
29
+ const app = JSON.parse(await fs.promises.readFile(path.join(buildDirectory, 'app.json'), 'utf8'));
30
+ const types = JSON.parse(await fs.promises.readFile(path.join(buildDirectory, 'types.json'), 'utf8'));
31
+ return {
32
+ notifications,
33
+ appEmail: app.email ?? {},
34
+ notificationTypes: types.notifications ?? {}
35
+ };
36
+ }
37
+ export default readNotificationArtifacts;
@@ -0,0 +1,47 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import fs from 'fs';
16
+ import path from 'path';
17
+ import { spawnProcess } from '@lowdefy/node-utils';
18
+ import { createStdOutLineHandler } from '@lowdefy/logger/cli';
19
+ async function runEmailPreview({ context }) {
20
+ const serverDirectory = context.directories.server;
21
+ const emailBin = path.join(serverDirectory, 'node_modules', '.bin', process.platform === 'win32' ? 'email.cmd' : 'email');
22
+ if (!fs.existsSync(emailBin)) {
23
+ throw new Error('react-email is not installed. Rerun "lowdefy emails" to install it.');
24
+ }
25
+ const port = context.options.port ?? 3001;
26
+ context.logger.info(`Starting the email preview server on port ${port}.`);
27
+ await spawnProcess({
28
+ command: emailBin,
29
+ args: [
30
+ 'dev',
31
+ '--dir',
32
+ context.directories.emails,
33
+ '--port',
34
+ String(port)
35
+ ],
36
+ stdOutLineHandler: createStdOutLineHandler({
37
+ context
38
+ }),
39
+ processOptions: {
40
+ cwd: serverDirectory,
41
+ shell: process.platform === 'win32',
42
+ env: process.env
43
+ },
44
+ silent: false
45
+ });
46
+ }
47
+ export default runEmailPreview;
@@ -0,0 +1,45 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import path from 'path';
16
+ import { createRequire } from 'module';
17
+ import { pathToFileURL } from 'url';
18
+ // A template documents the data keys it renders (Template.dataKeys). A key
19
+ // missing from testData means the preview section renders empty — usually a
20
+ // pipeline/template naming mismatch, surfaced here instead of as a silently
21
+ // empty email.
22
+ async function warnMissingDataKeys({ context, notifications, notificationTypes }) {
23
+ const serverRequire = createRequire(path.join(context.directories.server, 'package.json'));
24
+ const templateModules = {};
25
+ for (const notification of notifications){
26
+ const typeDefinition = notificationTypes[notification.type];
27
+ if (!typeDefinition) continue;
28
+ try {
29
+ if (!templateModules[typeDefinition.package]) {
30
+ const modulePath = serverRequire.resolve(`${typeDefinition.package}/notifications`);
31
+ templateModules[typeDefinition.package] = await import(pathToFileURL(modulePath));
32
+ }
33
+ const Template = templateModules[typeDefinition.package][typeDefinition.originalTypeName];
34
+ const testDataKeys = Object.keys(notification.testData ?? {});
35
+ (Template?.dataKeys ?? []).forEach((key)=>{
36
+ if (!testDataKeys.includes(key)) {
37
+ context.logger.warn(`Notification "${notification.notificationId}": template ${notification.type} renders "data.${key}" but testData has no "${key}" key — the preview section will be empty.`);
38
+ }
39
+ });
40
+ } catch (error) {
41
+ context.logger.debug(`Could not inspect template "${notification.type}" for data keys: ${error.message}`);
42
+ }
43
+ }
44
+ }
45
+ export default warnMissingDataKeys;
@@ -1,34 +1,52 @@
1
- FROM node:18-buster AS builder
1
+ # syntax=docker/dockerfile:1
2
+
3
+ FROM node:22-slim AS builder
2
4
 
3
5
  WORKDIR /lowdefy
4
6
 
5
- COPY . .
6
- # Configure standalone next build
7
- ENV LOWDEFY_BUILD_OUTPUT_STANDALONE 1
7
+ # pnpm is required by the Lowdefy build to install server dependencies. Installed
8
+ # through npm rather than corepack — corepack is not bundled from Node.js 25.
9
+ RUN npm install --global pnpm@10
8
10
 
9
- # Enable pnpm using corepack
10
- RUN corepack enable
11
+ # pnpm stores packages in $PNPM_HOME/store — mounted as a BuildKit cache below so
12
+ # repeat builds only download new or changed packages.
13
+ ENV PNPM_HOME=/pnpm
14
+ ENV PATH="$PNPM_HOME/bin:$PATH"
11
15
 
12
- # Build lowdefy app
13
- RUN pnpx lowdefy@4 build --log-level=debug
16
+ COPY . .
14
17
 
15
- FROM node:18-alpine AS runner
18
+ # Build the app then assemble a minimal runtime at .lowdefy/docker. docker-output
19
+ # traces the server's actual dependency graph (via @vercel/nft), so the runtime
20
+ # ships only the files the server imports — not the full node_modules, which
21
+ # includes the client block packages already compiled into dist/client. The CLI
22
+ # is pinned to the `lowdefy:` version in lowdefy.yaml so builds are reproducible.
23
+ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
24
+ LOWDEFY_VERSION=$(sed -nE "s/^lowdefy:(.*)/\1/p" lowdefy.yaml lowdefy.yml 2>/dev/null | tr -d '[:space:]') \
25
+ && test -n "$LOWDEFY_VERSION" || { echo "Could not read the lowdefy version from lowdefy.yaml"; exit 1; } \
26
+ && npx lowdefy@"$LOWDEFY_VERSION" build \
27
+ && npx lowdefy@"$LOWDEFY_VERSION" docker-output
16
28
 
17
- ENV NODE_ENV production
18
- ENV NEXT_TELEMETRY_DISABLED 1
29
+ FROM node:22-slim AS runner
19
30
 
20
- WORKDIR /lowdefy
31
+ ENV NODE_ENV=production
32
+ ENV PORT=3000
21
33
 
22
- RUN addgroup --system --gid 1001 nodejs
23
- RUN adduser --system --uid 1001 lowdefy
34
+ WORKDIR /lowdefy
24
35
 
25
- COPY --from=builder /lowdefy/.lowdefy/server/public ./public
26
- COPY --from=builder --chown=lowdefy:nodejs /lowdefy/.lowdefy/server/.next/standalone ./
36
+ # The node user (uid 1000) ships with the official Node.js images.
37
+ COPY --from=builder --chown=node:node /lowdefy/.lowdefy/docker ./
27
38
 
28
- USER lowdefy
39
+ USER node
29
40
 
30
41
  EXPOSE 3000
31
42
 
32
- ENV PORT 3000
43
+ # The Lowdefy server serves a liveness endpoint at /api/lowdefy-health. If your app sets a
44
+ # basePath, prepend it to the path below. Orchestrators with their own probes
45
+ # (e.g. Kubernetes) ignore this and should point their probes at the same path.
46
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
47
+ CMD ["node", "-e", "fetch('http://localhost:'+(process.env.PORT||3000)+'/api/lowdefy-health').then((res)=>process.exit(res.ok?0:1)).catch(()=>process.exit(1))"]
33
48
 
34
- CMD ["node", "server.js"]
49
+ # start.mjs (written by docker-output) chdirs into the server directory and boots
50
+ # it. Exec form keeps node as PID 1 — the server handles SIGTERM/SIGINT for
51
+ # graceful shutdown, so no init wrapper is needed.
52
+ CMD ["node", "start.mjs"]
@@ -0,0 +1,8 @@
1
+ .git
2
+ .lowdefy
3
+ .env
4
+ .env.*
5
+ node_modules
6
+ Dockerfile
7
+ .dockerignore
8
+ README.md
@@ -20,7 +20,9 @@ async function initDocker({ context }) {
20
20
  const dockerfile = await readFile(url.fileURLToPath(new URL('./Dockerfile', import.meta.url)));
21
21
  await writeFile(path.join(context.directories.config, 'Dockerfile'), dockerfile);
22
22
  context.logger.info("Created 'Dockerfile'.");
23
- const dockerignore = await readFile(url.fileURLToPath(new URL('./.dockerignore', import.meta.url)));
23
+ // Template stored without the leading dot — swc --copy-files drops dotfiles,
24
+ // so a .dockerignore template would be missing from the published package.
25
+ const dockerignore = await readFile(url.fileURLToPath(new URL('./dockerignore', import.meta.url)));
24
26
  await writeFile(path.join(context.directories.config, '.dockerignore'), dockerignore);
25
27
  context.logger.info("Created '.dockerignore'.");
26
28
  await context.sendTelemetry();