@nosto/nosto-cli 1.0.1

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 (88) hide show
  1. package/.github/copilot-instructions.md +326 -0
  2. package/.github/dependabot.yml +9 -0
  3. package/.github/pull_request_template.md +12 -0
  4. package/.github/workflows/ci.yml +58 -0
  5. package/.github/workflows/release.yml +49 -0
  6. package/.husky/commit-msg +1 -0
  7. package/.prettierrc +9 -0
  8. package/LICENSE +29 -0
  9. package/README.md +154 -0
  10. package/commitlint.config.js +4 -0
  11. package/eslint.config.js +36 -0
  12. package/package.json +63 -0
  13. package/src/api/library/fetchLibraryFile.ts +18 -0
  14. package/src/api/retry.ts +28 -0
  15. package/src/api/source/fetchSourceFile.ts +33 -0
  16. package/src/api/source/listSourceFiles.ts +13 -0
  17. package/src/api/source/putSourceFile.ts +14 -0
  18. package/src/api/source/schema.ts +10 -0
  19. package/src/api/utils.ts +52 -0
  20. package/src/bootstrap.sh +26 -0
  21. package/src/commander.ts +119 -0
  22. package/src/config/authConfig.ts +42 -0
  23. package/src/config/config.ts +109 -0
  24. package/src/config/envConfig.ts +23 -0
  25. package/src/config/fileConfig.ts +39 -0
  26. package/src/config/schema.ts +70 -0
  27. package/src/config/searchTemplatesConfig.ts +33 -0
  28. package/src/console/logger.ts +93 -0
  29. package/src/console/userPrompt.ts +16 -0
  30. package/src/errors/InvalidLoginResponseError.ts +14 -0
  31. package/src/errors/MissingConfigurationError.ts +14 -0
  32. package/src/errors/NostoError.ts +13 -0
  33. package/src/errors/NotNostoTemplateError.ts +15 -0
  34. package/src/errors/withErrorHandler.ts +50 -0
  35. package/src/exports.ts +8 -0
  36. package/src/filesystem/asserts/assertGitRepo.ts +19 -0
  37. package/src/filesystem/asserts/assertNostoTemplate.ts +34 -0
  38. package/src/filesystem/calculateTreeHash.ts +28 -0
  39. package/src/filesystem/esbuild.ts +37 -0
  40. package/src/filesystem/esbuildPlugins.ts +72 -0
  41. package/src/filesystem/filesystem.ts +40 -0
  42. package/src/filesystem/isIgnored.ts +65 -0
  43. package/src/filesystem/legacyUtils.ts +10 -0
  44. package/src/filesystem/loadLibrary.ts +31 -0
  45. package/src/filesystem/processInBatches.ts +38 -0
  46. package/src/filesystem/utils/getLoaderScript.ts +28 -0
  47. package/src/index.ts +3 -0
  48. package/src/modules/login.ts +87 -0
  49. package/src/modules/logout.ts +13 -0
  50. package/src/modules/search-templates/build.ts +61 -0
  51. package/src/modules/search-templates/dev.ts +50 -0
  52. package/src/modules/search-templates/pull.ts +89 -0
  53. package/src/modules/search-templates/push.ts +121 -0
  54. package/src/modules/setup.ts +96 -0
  55. package/src/modules/status.ts +71 -0
  56. package/src/utils/withSafeEnvironment.ts +22 -0
  57. package/test/api/fetchSourceFile.test.ts +30 -0
  58. package/test/api/putSourceFile.test.ts +34 -0
  59. package/test/api/retry.test.ts +102 -0
  60. package/test/api/utils.test.ts +27 -0
  61. package/test/commander.test.ts +271 -0
  62. package/test/config/envConfig.test.ts +62 -0
  63. package/test/config/fileConfig.test.ts +63 -0
  64. package/test/config/schema.test.ts +96 -0
  65. package/test/config/searchTemplatesConfig.test.ts +43 -0
  66. package/test/console/logger.test.ts +96 -0
  67. package/test/errors/withErrorHandler.test.ts +64 -0
  68. package/test/filesystem/filesystem.test.ts +53 -0
  69. package/test/filesystem/plugins.test.ts +35 -0
  70. package/test/index.test.ts +15 -0
  71. package/test/modules/search-templates/build.legacy.test.ts +74 -0
  72. package/test/modules/search-templates/build.modern.test.ts +33 -0
  73. package/test/modules/search-templates/dev.legacy.test.ts +75 -0
  74. package/test/modules/search-templates/dev.modern.test.ts +44 -0
  75. package/test/modules/search-templates/pull.test.ts +96 -0
  76. package/test/modules/search-templates/push.test.ts +109 -0
  77. package/test/modules/setup.test.ts +49 -0
  78. package/test/modules/status.test.ts +22 -0
  79. package/test/setup.ts +28 -0
  80. package/test/utils/generateEndpointMock.ts +60 -0
  81. package/test/utils/mockCommander.ts +22 -0
  82. package/test/utils/mockConfig.ts +37 -0
  83. package/test/utils/mockConsole.ts +65 -0
  84. package/test/utils/mockFileSystem.ts +52 -0
  85. package/test/utils/mockServer.ts +76 -0
  86. package/test/utils/mockStarterManifest.ts +42 -0
  87. package/tsconfig.json +20 -0
  88. package/vitest.config.ts +33 -0
@@ -0,0 +1,70 @@
1
+ import z from "zod"
2
+
3
+ export const LogLevel = ["debug", "info", "warn", "error"] as const
4
+
5
+ // Persistent per-repo config (.nosto.json file)
6
+ export const PersistentConfigSchema = z.object({
7
+ merchant: z.string(),
8
+ apiKey: z.string().optional(),
9
+ templatesEnv: z.string().default("main"),
10
+ apiUrl: z.string().default("https://api.nosto.com"),
11
+ libraryUrl: z.string().default("https://d11ffvpvtnmt0d.cloudfront.net/library"),
12
+ logLevel: z.enum(LogLevel).default("info"),
13
+ maxRequests: z.coerce.number().default(15)
14
+ })
15
+
16
+ // Config provided on runtime (CLI options)
17
+ export const RuntimeConfigSchema = z.object({
18
+ projectPath: z.string().default("."),
19
+ dryRun: z.boolean().default(false),
20
+ verbose: z.boolean().default(false)
21
+ })
22
+
23
+ // Authentication config (~/nosto/.auth.json file)
24
+ export const AuthConfigSchema = z.object({
25
+ user: z.string(),
26
+ token: z.string(),
27
+ expiresAt: z.coerce.date()
28
+ })
29
+
30
+ // Environmental variables, alternative to persistent config
31
+ export const EnvironmentConfigSchema = z.object({
32
+ merchant: z.string().optional(),
33
+ apiKey: z.string().optional(),
34
+ templatesEnv: z.string().optional(),
35
+ apiUrl: z.string().optional(),
36
+ libraryUrl: z.string().optional(),
37
+ logLevel: z.enum(LogLevel).optional(),
38
+ maxRequests: z.coerce.number().optional()
39
+ })
40
+
41
+ // Modern config, can be committed to repo (nosto.config.ts file)
42
+ export const SearchTemplatesConfigSchema = z.object({
43
+ onBuild: z
44
+ .custom<() => Promise<void>>((val): val is () => Promise<void> => typeof val === "function")
45
+ .default(() => async () => {
46
+ throw new Error("onBuild function not implemented")
47
+ }),
48
+ onBuildWatch: z
49
+ .custom<(props: OnStartDevProps) => Promise<void>>(
50
+ (val): val is (props: OnStartDevProps) => Promise<void> => typeof val === "function"
51
+ )
52
+ .default(() => async () => {
53
+ throw new Error("onBuildWatch function not implemented")
54
+ })
55
+ })
56
+ export type OnStartDevProps = {
57
+ onAfterBuild: () => Promise<void>
58
+ }
59
+
60
+ export type SearchTemplatesMode = "modern" | "legacy" | "unknown"
61
+ export type Config = PersistentConfig &
62
+ RuntimeConfig & { auth: AuthConfig; searchTemplates: { mode: SearchTemplatesMode; data: SearchTemplatesConfig } }
63
+ export type PersistentConfig = z.infer<typeof PersistentConfigSchema>
64
+ export type RuntimeConfig = z.infer<typeof RuntimeConfigSchema>
65
+ export type AuthConfig = z.infer<typeof AuthConfigSchema>
66
+ export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>
67
+ export type SearchTemplatesConfig = z.infer<typeof SearchTemplatesConfigSchema>
68
+
69
+ export const PartialPersistentConfigSchema = PersistentConfigSchema.partial()
70
+ export type PartialPersistentConfig = z.infer<typeof PartialPersistentConfigSchema>
@@ -0,0 +1,33 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import z from "zod"
4
+
5
+ import { NostoError } from "#errors/NostoError.ts"
6
+
7
+ import { type SearchTemplatesConfig, SearchTemplatesConfigSchema, type SearchTemplatesMode } from "./schema.ts"
8
+
9
+ export async function parseSearchTemplatesConfigFile({
10
+ projectPath
11
+ }: {
12
+ projectPath: string
13
+ }): Promise<{ mode: SearchTemplatesMode; data: SearchTemplatesConfig }> {
14
+ const configPath = path.resolve(projectPath, "nosto.config.ts")
15
+
16
+ // If config is not present, assume legacy mode
17
+ if (!fs.existsSync(configPath)) {
18
+ return {
19
+ mode: "legacy",
20
+ data: SearchTemplatesConfigSchema.parse({})
21
+ }
22
+ }
23
+
24
+ const defaultExport = await import(configPath).then(module => module.default)
25
+ const parsedScriptObject = SearchTemplatesConfigSchema.strict().safeParse(defaultExport)
26
+ if (!parsedScriptObject.success) {
27
+ throw new NostoError("Invalid nosto.config.ts file: " + z.treeifyError(parsedScriptObject.error))
28
+ }
29
+ return {
30
+ mode: "modern",
31
+ data: parsedScriptObject.data
32
+ }
33
+ }
@@ -0,0 +1,93 @@
1
+ import chalk from "chalk"
2
+
3
+ import { LogLevel } from "#config/schema.ts"
4
+
5
+ const formatTimestamp = (date: Date): string => {
6
+ const pad = (num: number) => num.toString().padStart(2, "0")
7
+ const hours = pad(date.getHours())
8
+ const minutes = pad(date.getMinutes())
9
+ const seconds = pad(date.getSeconds())
10
+ return `${hours}:${minutes}:${seconds}`
11
+ }
12
+
13
+ const Presets = {
14
+ debug: () => ({
15
+ color: chalk.gray,
16
+ prefix: chalk.gray("[DEBUG]"),
17
+ logger: console.debug,
18
+ logLevel: 0
19
+ }),
20
+ info: () => ({
21
+ color: chalk.white,
22
+ prefix: chalk.white("[INFO] "),
23
+ logger: console.info,
24
+ logLevel: 1
25
+ }),
26
+ success: () => ({
27
+ color: chalk.green,
28
+ prefix: chalk.green("[SUCCESS] "),
29
+ logger: console.info,
30
+ logLevel: 1
31
+ }),
32
+ warn: () => ({
33
+ color: chalk.yellow,
34
+ prefix: chalk.yellow("[WARN] "),
35
+ logger: console.warn,
36
+ logLevel: 2
37
+ }),
38
+ error: () => ({
39
+ color: chalk.red,
40
+ prefix: chalk.red("[ERROR]"),
41
+ logger: console.error,
42
+ logLevel: 3
43
+ })
44
+ }
45
+
46
+ const printToLog = (message: string, presetFunc: (typeof Presets)[keyof typeof Presets], extra?: unknown) => {
47
+ const targetLogLevel = LogLevel.indexOf(Logger.context.logLevel)
48
+ const preset = presetFunc()
49
+ if (targetLogLevel > preset.logLevel) {
50
+ return
51
+ }
52
+ const timestamp = chalk.dim(formatTimestamp(new Date()))
53
+ const merchantId = Logger.context.merchantId ? `[${chalk.greenBright(Logger.context.merchantId)}] ` : "| "
54
+ const dryRun = Logger.context.isDryRun ? chalk.dim("(DRY RUN) ") : ""
55
+ preset.logger(`${timestamp} ${dryRun}${merchantId}${preset.color(message)}`)
56
+ if (extra) {
57
+ preset.logger(chalk.dim(extra, null, 2))
58
+ }
59
+ }
60
+
61
+ export const Logger = {
62
+ context: {
63
+ logLevel: LogLevel[1] as (typeof LogLevel)[number],
64
+ merchantId: "",
65
+ isDryRun: false
66
+ },
67
+ raw: (message: string, extra?: unknown) => {
68
+ console.log(message)
69
+ if (extra) {
70
+ console.log(extra)
71
+ }
72
+ },
73
+
74
+ debug: (message: string, extra?: unknown) => {
75
+ printToLog(message, Presets.debug, extra)
76
+ },
77
+
78
+ info: (message: string, extra?: unknown) => {
79
+ printToLog(message, Presets.info, extra)
80
+ },
81
+
82
+ success: (message: string, extra?: unknown) => {
83
+ printToLog(message, Presets.success, extra)
84
+ },
85
+
86
+ warn: (message: string, extra?: unknown) => {
87
+ printToLog(message, Presets.warn, extra)
88
+ },
89
+
90
+ error: (message: string, extra?: unknown) => {
91
+ printToLog(message, Presets.error, extra)
92
+ }
93
+ }
@@ -0,0 +1,16 @@
1
+ import chalk from "chalk"
2
+ import { createInterface } from "readline/promises"
3
+
4
+ export async function promptForConfirmation(message: string, defaultAnswer: "Y" | "N"): Promise<boolean> {
5
+ const rl = createInterface({
6
+ input: process.stdin,
7
+ output: process.stdout
8
+ })
9
+
10
+ const ynPrompt = defaultAnswer === "Y" ? " (Y/n): " : " (y/N): "
11
+
12
+ const answer = await rl.question("\n" + message + chalk.gray(ynPrompt))
13
+ rl.close()
14
+ const evaluatedAnswer = answer.length === 0 ? defaultAnswer : answer.toUpperCase()
15
+ return evaluatedAnswer === "Y"
16
+ }
@@ -0,0 +1,14 @@
1
+ import { Logger } from "#console/logger.ts"
2
+
3
+ import { NostoError } from "./NostoError.ts"
4
+
5
+ export class InvalidLoginResponseError extends NostoError {
6
+ constructor(message: string) {
7
+ super(message)
8
+ this.name = "InvalidLoginResponseError"
9
+ }
10
+
11
+ handle() {
12
+ Logger.error(this.message)
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ import { Logger } from "#console/logger.ts"
2
+
3
+ import { NostoError } from "./NostoError.ts"
4
+
5
+ export class MissingConfigurationError extends NostoError {
6
+ constructor(message: string) {
7
+ super(message)
8
+ this.name = "MissingConfigurationError"
9
+ }
10
+
11
+ handle() {
12
+ Logger.error(this.message)
13
+ }
14
+ }
@@ -0,0 +1,13 @@
1
+ import { Logger } from "#console/logger.ts"
2
+
3
+ export class NostoError extends Error {
4
+ constructor(message: string) {
5
+ super(message)
6
+ this.name = "NostoError"
7
+ }
8
+
9
+ handle() {
10
+ Logger.error(`Generic Nosto error:`)
11
+ Logger.error(`- ${this.message}`)
12
+ }
13
+ }
@@ -0,0 +1,15 @@
1
+ import { Logger } from "#console/logger.ts"
2
+
3
+ import { NostoError } from "./NostoError.ts"
4
+
5
+ export class NotNostoTemplateError extends NostoError {
6
+ constructor(message: string) {
7
+ super(message)
8
+ this.name = "NotNostoTemplateError"
9
+ }
10
+
11
+ handle() {
12
+ Logger.error(`This doesn't seem to be a Nosto template folder.`)
13
+ Logger.error(`- ${this.message}`)
14
+ }
15
+ }
@@ -0,0 +1,50 @@
1
+ import chalk from "chalk"
2
+ import { HTTPError, TimeoutError } from "ky"
3
+
4
+ import { getCachedConfig } from "#config/config.ts"
5
+ import { Logger } from "#console/logger.ts"
6
+
7
+ import { InvalidLoginResponseError } from "./InvalidLoginResponseError.ts"
8
+ import { NostoError } from "./NostoError.ts"
9
+
10
+ export async function withErrorHandler(fn: () => void | Promise<void>): Promise<void> {
11
+ try {
12
+ await fn()
13
+ } catch (error) {
14
+ if (error instanceof HTTPError) {
15
+ const config = getCachedConfig()
16
+ Logger.error(`HTTP Request failed:`)
17
+ Logger.error(`- ${error.response.status} ${error.response.statusText}`)
18
+ Logger.error(`- ${error.request.method} ${error.request.url}`)
19
+ if (config.verbose) {
20
+ Logger.raw(chalk.red(prettyPrintStack(error.stack)))
21
+ }
22
+ if (!config.verbose) {
23
+ Logger.info(chalk.gray("Rerun with --verbose to see details"))
24
+ }
25
+ } else if (error instanceof TimeoutError) {
26
+ const config = getCachedConfig()
27
+ Logger.error(`HTTP Request timed out:`)
28
+ Logger.error(`- Server did not respond after 10 seconds`)
29
+ Logger.error(`- ${error.request.method} ${error.request.url}`)
30
+ if (config.verbose) {
31
+ Logger.raw(chalk.red(prettyPrintStack(error.stack)))
32
+ }
33
+ } else if (error instanceof InvalidLoginResponseError) {
34
+ Logger.error(`Received malformed login response from server. This is probably a bug on our side.`, error)
35
+ } else if (error instanceof NostoError) {
36
+ error.handle()
37
+ } else {
38
+ throw error
39
+ }
40
+ }
41
+ }
42
+
43
+ export function prettyPrintStack(stack: string | undefined) {
44
+ if (!stack) {
45
+ return ""
46
+ }
47
+ const lines = stack.split("\n")
48
+ const filteredLines = lines.filter(line => line.includes(".ts"))
49
+ return filteredLines.join("\n")
50
+ }
package/src/exports.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { SearchTemplatesConfig } from "#config/schema.ts"
2
+
3
+ /**
4
+ * No-op helper to enforce type safety.
5
+ */
6
+ export function makeConfig(args: SearchTemplatesConfig) {
7
+ return args
8
+ }
@@ -0,0 +1,19 @@
1
+ import chalk from "chalk"
2
+ import fs from "fs"
3
+ import path from "path"
4
+
5
+ import { getCachedConfig } from "#config/config.ts"
6
+ import { Logger } from "#console/logger.ts"
7
+
8
+ export function assertGitRepo() {
9
+ const { projectPath } = getCachedConfig()
10
+
11
+ if (fs.existsSync(path.join(projectPath, ".git"))) {
12
+ return
13
+ }
14
+
15
+ const gitCommand = projectPath === "." ? "git init" : `cd ${projectPath} && git init`
16
+ Logger.warn(
17
+ `We heavily recommend using git for your projects. You can start with just running ${chalk.blueBright(gitCommand)}`
18
+ )
19
+ }
@@ -0,0 +1,34 @@
1
+ import chalk from "chalk"
2
+ import fs from "fs"
3
+ import path from "path"
4
+
5
+ import { getCachedConfig } from "#config/config.ts"
6
+ import { Logger } from "#console/logger.ts"
7
+ import { NotNostoTemplateError } from "#errors/NotNostoTemplateError.ts"
8
+ import { isModernTemplateProject } from "#filesystem/legacyUtils.ts"
9
+
10
+ export function assertNostoTemplate() {
11
+ const { projectPath } = getCachedConfig()
12
+
13
+ Logger.debug("Sanity checking the target directory...")
14
+ const targetFolder = path.resolve(projectPath)
15
+ if (!fs.existsSync(targetFolder)) {
16
+ throw new Error(`Target folder does not exist: ${chalk.cyan(targetFolder)}`)
17
+ }
18
+ if (!fs.statSync(targetFolder).isDirectory()) {
19
+ throw new Error(`Target path is not a directory: ${chalk.cyan(targetFolder)}`)
20
+ }
21
+
22
+ if (isModernTemplateProject()) {
23
+ return
24
+ }
25
+
26
+ const indexFilePath = path.join(projectPath, "index.js")
27
+ if (!fs.existsSync(indexFilePath)) {
28
+ throw new NotNostoTemplateError(`Index file does not exist: ${indexFilePath}`)
29
+ }
30
+ const indexFileContent = fs.readFileSync(indexFilePath, "utf-8")
31
+ if (!indexFileContent.includes("@nosto/preact")) {
32
+ throw new NotNostoTemplateError(`Index file does not contain @nosto/preact: ${indexFilePath}`)
33
+ }
34
+ }
@@ -0,0 +1,28 @@
1
+ import crypto from "crypto"
2
+ import fs from "fs"
3
+
4
+ import { getCachedConfig } from "#config/config.ts"
5
+
6
+ import { getIgnoreInstance } from "./isIgnored.ts"
7
+
8
+ export function calculateTreeHash() {
9
+ const hash = crypto.createHash("sha256")
10
+ for (const filePath of listAllFilesForHashing()) {
11
+ const fileContent = fs.readFileSync(filePath, "utf-8")
12
+ hash.update(fileContent)
13
+ }
14
+ return hash.digest("hex")
15
+ }
16
+
17
+ export function listAllFilesForHashing() {
18
+ const { projectPath } = getCachedConfig()
19
+ const allFiles = fs.readdirSync(projectPath, { withFileTypes: true, recursive: true })
20
+ const ignoreInstance = getIgnoreInstance()
21
+ const filteredFiles = allFiles
22
+ .filter(dirent => !ignoreInstance.isIgnored(dirent))
23
+ .map(dirent => dirent.parentPath + "/" + dirent.name)
24
+ // To relative path
25
+ .map(file => file.replace(projectPath + "/", ""))
26
+ .filter(file => file !== "build/hash")
27
+ return filteredFiles
28
+ }
@@ -0,0 +1,37 @@
1
+ import { context, type Plugin } from "esbuild"
2
+ import path from "path"
3
+
4
+ import { getCachedConfig } from "#config/config.ts"
5
+
6
+ import { createLoaderPlugin, notifyOnRebuildPlugin } from "./esbuildPlugins.ts"
7
+
8
+ export type EsbuildContextOptions = {
9
+ plugins?: Plugin[]
10
+ }
11
+
12
+ export function getBuildContext(options: EsbuildContextOptions = {}) {
13
+ const { plugins = [] } = options
14
+ const { projectPath } = getCachedConfig()
15
+ return context({
16
+ bundle: true,
17
+ minify: true,
18
+ write: true,
19
+ nodePaths: [path.resolve(import.meta.dirname, "../../node_modules")],
20
+ sourcemap: "linked",
21
+ treeShaking: true,
22
+ jsx: "automatic",
23
+ jsxImportSource: "preact",
24
+ jsxFragment: "Fragment",
25
+ metafile: true,
26
+ target: ["es2018"],
27
+ supported: {
28
+ nesting: false // transpile nested CSS to flat
29
+ },
30
+ outfile: path.resolve(projectPath, "build/bundle.js"),
31
+ entryPoints: [path.resolve(projectPath, "index.js")],
32
+ alias: {
33
+ "@nosto/preact": path.resolve(projectPath, ".nostocache/library/nosto.module.js")
34
+ },
35
+ plugins: [createLoaderPlugin(), notifyOnRebuildPlugin(), ...plugins]
36
+ })
37
+ }
@@ -0,0 +1,72 @@
1
+ import path from "node:path"
2
+
3
+ import chalk from "chalk"
4
+ import * as esbuild from "esbuild"
5
+ import fs from "fs"
6
+
7
+ import { getCachedConfig } from "#config/config.ts"
8
+ import { Logger } from "#console/logger.ts"
9
+ import { getLoaderScript } from "#filesystem/utils/getLoaderScript.ts"
10
+ import { pushSearchTemplate } from "#modules/search-templates/push.ts"
11
+
12
+ export function createLoaderPlugin(): esbuild.Plugin {
13
+ return {
14
+ name: "create-loader",
15
+ setup(build) {
16
+ build.onEnd(result => {
17
+ if (result.errors.length) {
18
+ return
19
+ }
20
+ Logger.debug("Generating loader script...")
21
+ const { projectPath } = getCachedConfig()
22
+ const outUri = path.join(projectPath, "build")
23
+ fs.writeFileSync(path.join(outUri, "loader.js"), getLoaderScript())
24
+ })
25
+ }
26
+ }
27
+ }
28
+
29
+ /**
30
+ * By default, esbuild is silent. This plugin prints a log message with some rebuild stats.
31
+ */
32
+ const performanceStats = {
33
+ lastBuildStartedAt: 0
34
+ }
35
+ export function notifyOnRebuildPlugin(): esbuild.Plugin {
36
+ return {
37
+ name: "notify-on-rebuild",
38
+ setup(build) {
39
+ build.onStart(() => {
40
+ performanceStats.lastBuildStartedAt = performance.now()
41
+ })
42
+ build.onEnd(result => {
43
+ if (result.errors.length) {
44
+ return
45
+ }
46
+ const duration = performance.now() - performanceStats.lastBuildStartedAt
47
+ Logger.info(`Templates built in ${chalk.green(Math.round(duration) + " ms")}.`)
48
+ })
49
+ }
50
+ }
51
+ }
52
+
53
+ export function pushOnRebuildPlugin(): esbuild.Plugin {
54
+ return {
55
+ name: "push-on-rebuild",
56
+ setup(build) {
57
+ build.onEnd(result => {
58
+ if (result.errors.length) {
59
+ return
60
+ }
61
+
62
+ // Only push built files
63
+ const { projectPath } = getCachedConfig()
64
+ const buildPath = path.resolve(projectPath, "build")
65
+ const files = fs.readdirSync(buildPath)
66
+ const paths = files.map(file => path.relative(projectPath, path.resolve(buildPath, file)))
67
+
68
+ pushSearchTemplate({ paths, force: false })
69
+ })
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,40 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+
4
+ import { getCachedConfig } from "#config/config.ts"
5
+ import { Logger } from "#console/logger.ts"
6
+
7
+ import { getIgnoreInstance } from "./isIgnored.ts"
8
+
9
+ export function writeFile(pathToWrite: string, data: string) {
10
+ if (getCachedConfig().dryRun) {
11
+ return
12
+ }
13
+ if (!fs.existsSync(path.dirname(pathToWrite))) {
14
+ Logger.debug(`Creating directory: ${path.dirname(pathToWrite)}`)
15
+ fs.mkdirSync(path.dirname(pathToWrite), { recursive: true })
16
+ }
17
+ Logger.debug(`Writing to file: ${pathToWrite}`)
18
+ fs.writeFileSync(pathToWrite, data)
19
+ }
20
+
21
+ export function readFileIfExists(pathToRead: string) {
22
+ if (fs.existsSync(pathToRead)) {
23
+ return fs.readFileSync(pathToRead, "utf-8")
24
+ }
25
+ return null
26
+ }
27
+
28
+ export function listAllFiles(folder: string) {
29
+ const allDirents = fs.readdirSync(folder, { withFileTypes: true, recursive: true })
30
+ const allFiles = allDirents.filter(dirent => !dirent.isDirectory())
31
+ const ignoreInstance = getIgnoreInstance()
32
+ const filteredFiles = allFiles
33
+ .filter(dirent => !ignoreInstance.isIgnored(dirent))
34
+ .map(dirent => dirent.parentPath + "/" + dirent.name)
35
+ .map(file => path.relative(folder, file))
36
+ return {
37
+ allFiles: filteredFiles,
38
+ unfilteredFileCount: allFiles.length
39
+ }
40
+ }
@@ -0,0 +1,65 @@
1
+ import { Dirent } from "fs"
2
+ import fs from "fs"
3
+ import type { Ignore } from "ignore"
4
+ import ignore from "ignore"
5
+ import path from "path"
6
+
7
+ import { getCachedConfig } from "#config/config.ts"
8
+
9
+ /**
10
+ * The build directory is never ignored even if it's in the .gitignore file
11
+ */
12
+ function isNeverIgnored(dirent: Dirent): boolean {
13
+ const { projectPath } = getCachedConfig()
14
+ const absoluteProjectPath = path.resolve(projectPath)
15
+
16
+ return (
17
+ dirent.parentPath.startsWith(path.join(projectPath, "build")) ||
18
+ dirent.parentPath.startsWith(path.join(absoluteProjectPath, "build"))
19
+ )
20
+ }
21
+
22
+ function isIgnoredImplicitly(dirent: Dirent): boolean {
23
+ const parentPathSections = dirent.parentPath.split("/")
24
+ if (
25
+ !dirent.isFile() ||
26
+ dirent.name.startsWith(".") ||
27
+ dirent.name.includes("node_modules") ||
28
+ parentPathSections.some(section => section !== "." && section.startsWith("."))
29
+ ) {
30
+ return true
31
+ }
32
+
33
+ return false
34
+ }
35
+
36
+ export function isIgnoredExplicitly(instance: Ignore, dirent: Dirent): boolean {
37
+ const { projectPath } = getCachedConfig()
38
+ const absoluteProjectPath = path.resolve(projectPath)
39
+ const relativePath = dirent.parentPath
40
+ ? path.relative(absoluteProjectPath, path.join(dirent.parentPath, dirent.name))
41
+ : dirent.name
42
+ return instance.ignores(relativePath)
43
+ }
44
+
45
+ export function getIgnoreInstance() {
46
+ const { projectPath } = getCachedConfig()
47
+
48
+ const gitignorePath = path.join(projectPath, ".gitignore")
49
+ const patterns: string[] = fs.existsSync(gitignorePath)
50
+ ? fs
51
+ .readFileSync(gitignorePath, "utf-8")
52
+ .split(/\r?\n/)
53
+ .filter(line => line.trim() && !line.startsWith("#"))
54
+ : []
55
+
56
+ const instance = ignore().add(patterns)
57
+ return {
58
+ isIgnored: (dirent: Dirent) => {
59
+ if (isNeverIgnored(dirent)) {
60
+ return false
61
+ }
62
+ return isIgnoredImplicitly(dirent) || isIgnoredExplicitly(instance, dirent)
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,10 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+
4
+ import { getCachedConfig } from "#config/config.ts"
5
+
6
+ export function isModernTemplateProject() {
7
+ const { projectPath } = getCachedConfig()
8
+ const deployManifest = path.resolve(projectPath, "nosto.config.ts")
9
+ return fs.existsSync(deployManifest)
10
+ }
@@ -0,0 +1,31 @@
1
+ import chalk from "chalk"
2
+ import path from "path"
3
+
4
+ import { fetchLibraryFile } from "#api/library/fetchLibraryFile.ts"
5
+ import { fetchWithRetry } from "#api/retry.ts"
6
+ import { cleanUrl } from "#api/utils.ts"
7
+ import { Logger } from "#console/logger.ts"
8
+
9
+ import { writeFile } from "./filesystem.ts"
10
+
11
+ export async function loadLibrary(libraryPath: string) {
12
+ const filesToLoad = ["/nosto.module.js", "/nosto.module.js.map", "/nosto.d.ts"]
13
+ let filesFetched = 0
14
+ const fileCount = filesToLoad.length
15
+ const promises = filesToLoad.map(async filename => {
16
+ const data = await fetchWithRetry(fetchLibraryFile, filename)
17
+ filesFetched++
18
+ Logger.info(
19
+ `${chalk.green("✓")} [${filesFetched}/${fileCount}] ${chalk.blue("↓")} ${chalk.cyan(cleanUrl(filename))}`
20
+ )
21
+ return {
22
+ data,
23
+ filename
24
+ }
25
+ })
26
+ const files = await Promise.all(promises)
27
+ files.forEach(({ data, filename }) => {
28
+ const filePath = path.join(libraryPath, filename)
29
+ writeFile(filePath, data)
30
+ })
31
+ }