lowdefy 5.5.1 → 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.
- package/dist/commands/agentSetup/agentSetup.js +85 -0
- package/dist/commands/agentSetup/agentsMd.js +59 -0
- package/dist/commands/agentSetup/buildMcpServerEntry.js +21 -0
- package/dist/commands/agentSetup/findProjectRoot.js +39 -0
- package/dist/commands/agentSetup/getDevCommand.js +50 -0
- package/dist/commands/agentSetup/skillMd.js +48 -0
- package/dist/commands/agentSetup/upsertAgentsMdSection.js +63 -0
- package/dist/commands/agentSetup/upsertClaudeSettings.js +56 -0
- package/dist/commands/agentSetup/upsertMcpServer.js +83 -0
- package/dist/commands/agentSetup/writeSkillFile.js +32 -0
- package/dist/commands/build/build.js +8 -3
- package/dist/commands/dev/dev.js +16 -6
- package/dist/commands/dev/resolveMockUser.js +37 -0
- package/dist/commands/dev/runDevServer.js +17 -11
- package/dist/commands/dockerOutput/dockerOutput.js +99 -0
- package/dist/commands/emails/emails.js +84 -0
- package/dist/commands/emails/generateEmailShims.js +94 -0
- package/dist/commands/emails/installReactEmail.js +53 -0
- package/dist/commands/emails/readNotificationArtifacts.js +37 -0
- package/dist/commands/emails/runEmailPreview.js +47 -0
- package/dist/commands/emails/warnMissingDataKeys.js +45 -0
- package/dist/commands/init-docker/Dockerfile +37 -19
- package/dist/commands/init-docker/dockerignore +8 -0
- package/dist/commands/init-docker/initDocker.js +3 -1
- package/dist/commands/init-vercel/README.md +94 -20
- package/dist/commands/init-vercel/initVercel.js +12 -2
- package/dist/commands/init-vercel/vercel.build.sh +8 -0
- package/dist/commands/init-vercel/vercel.install.sh +4 -2
- package/dist/commands/init-vercel/vercel.json +5 -0
- package/dist/commands/start/runStart.js +1 -2
- package/dist/commands/vercelOutput/apiHandler.js +83 -0
- package/dist/commands/vercelOutput/vercelOutput.js +138 -0
- package/dist/program.js +24 -2
- package/dist/utils/checkPortAvailable.js +9 -16
- package/dist/utils/copyTracedFiles.js +94 -0
- package/dist/utils/ensurePnpmWorkspaceYaml.js +58 -0
- package/dist/utils/findPnpmWorkspaceRoot.js +29 -0
- package/dist/utils/findTraceBase.js +45 -0
- package/dist/utils/getDirectories.js +2 -1
- package/dist/utils/getOptions.js +2 -0
- package/dist/utils/installServer.js +31 -23
- package/dist/utils/{runNextBuild.js → runClientBuild.js} +11 -14
- 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
|
-
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
FROM node:22-slim AS builder
|
|
2
4
|
|
|
3
5
|
WORKDIR /lowdefy
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
#
|
|
7
|
-
|
|
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
|
-
#
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
RUN pnpx lowdefy@4 build --log-level=debug
|
|
16
|
+
COPY . .
|
|
14
17
|
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
ENV NEXT_TELEMETRY_DISABLED 1
|
|
29
|
+
FROM node:22-slim AS runner
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
ENV NODE_ENV=production
|
|
32
|
+
ENV PORT=3000
|
|
21
33
|
|
|
22
|
-
|
|
23
|
-
RUN adduser --system --uid 1001 lowdefy
|
|
34
|
+
WORKDIR /lowdefy
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
COPY --from=builder --chown=
|
|
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
|
|
39
|
+
USER node
|
|
29
40
|
|
|
30
41
|
EXPOSE 3000
|
|
31
42
|
|
|
32
|
-
|
|
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
|
-
|
|
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"]
|
|
@@ -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
|
-
|
|
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();
|